evolcore 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +58 -9
- package/bin/codex-managed-hook.mjs +3 -0
- package/bin/install-codex-managed-hooks.mjs +3 -1
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +393 -108
- package/dist/agents/codex-app-server-client.js +41 -7
- package/dist/agents/codex-runner.js +1292 -220
- package/dist/agents/ecagent-runner.js +171 -61
- 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 +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +189 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +618 -123
- 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 +98 -123
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +50 -0
- package/dist/cli/trigger-command.js +14 -5
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +19 -5
- package/dist/config/config-manager.js +167 -22
- package/dist/config/contact-book-store.js +25 -3
- 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 +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- 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 +43 -1
- package/dist/core/auth/auth-gateway.js +41 -4
- package/dist/core/auth/authorization-audit.js +216 -8
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +8 -0
- package/dist/core/bootstrap-service.js +99 -27
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +311 -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 +288 -80
- package/dist/core/command/menu-protocol.js +1 -1
- 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 +377 -36
- package/dist/core/data-migration.js +11 -1
- package/dist/core/event-catalog.js +37 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +33 -3
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +90 -87
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +184 -12
- package/dist/core/message/message-log.js +47 -7
- package/dist/core/message/message-queue.js +227 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +658 -109
- 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 +627 -69
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-error-code.js +12 -0
- package/dist/core/permission/tool-policy.js +618 -23
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +329 -30
- package/dist/core/session/session-renew.js +37 -13
- package/dist/core/session/session-turn-coordinator.js +16 -5
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +316 -50
- package/dist/ipc.js +459 -29
- package/dist/paths.js +82 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/response-system/engines/v1/proactive-flow.js +7 -2
- package/dist/stats/price-resolver.js +4 -0
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +70 -7
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +13 -3
- package/dist/trigger/scheduler.js +20 -3
- 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 +30 -6
- 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/tool-summary.js +59 -0
- package/dist/utils/windows-shell-trust.js +201 -0
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +3 -3
- package/kits/docs/evolcore/agent-create.md +146 -0
- package/kits/docs/evolcore/agent.md +6 -0
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/group-collaboration.md +251 -0
- package/kits/docs/evolcore/group-rules.md +1 -19
- package/kits/docs/evolcore/group.md +3 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/docs/evolcore/trigger.md +6 -3
- package/kits/docs/prompt-loading-architecture.md +6 -0
- package/kits/eck_message_manifest.json +6 -6
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/agent-config.schema.12.json +427 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- 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/message-fragments/item.md +1 -1
- 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/kits/templates/system-fragments/bootstrap.md +2 -1
- package/kits/templates/system-fragments/commands.md +2 -2
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
package/dist/aun/outbox.js
CHANGED
|
@@ -1,12 +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
|
|
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;
|
|
6
9
|
const MAX_TERMINAL_ENTRIES_PER_AID = 100;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
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';
|
|
10
33
|
}
|
|
11
34
|
function generateId() {
|
|
12
35
|
const ts = Date.now();
|
|
@@ -17,7 +40,7 @@ function isExpired(entry) {
|
|
|
17
40
|
// Terminal failures are retained as durable diagnostics. They are excluded
|
|
18
41
|
// from `load()`/`hasPending()` and must not disappear merely because the
|
|
19
42
|
// original delivery TTL elapsed.
|
|
20
|
-
if (entry.terminal)
|
|
43
|
+
if (entry.terminal || entry.deliveryResult)
|
|
21
44
|
return false;
|
|
22
45
|
if (entry.critical)
|
|
23
46
|
return false;
|
|
@@ -26,8 +49,8 @@ function isExpired(entry) {
|
|
|
26
49
|
export function isDeliveryForChannel(value, channelId) {
|
|
27
50
|
return isDeliveryTargetForChannel(value, channelId);
|
|
28
51
|
}
|
|
29
|
-
function readEntries(aid) {
|
|
30
|
-
const file = outboxFile(aid);
|
|
52
|
+
function readEntries(aid, queue = 'default') {
|
|
53
|
+
const file = outboxFile(aid, queue);
|
|
31
54
|
try {
|
|
32
55
|
const content = atomicRead(file)?.trim();
|
|
33
56
|
if (!content)
|
|
@@ -45,36 +68,45 @@ function readEntries(aid) {
|
|
|
45
68
|
return [];
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
|
-
function writeEntries(aid, entries) {
|
|
49
|
-
const file = outboxFile(aid);
|
|
50
|
-
const boundedEntries =
|
|
71
|
+
function writeEntries(aid, entries, queue = 'default') {
|
|
72
|
+
const file = outboxFile(aid, queue);
|
|
73
|
+
const boundedEntries = trimFinalEntries(entries);
|
|
51
74
|
const content = boundedEntries.length > 0
|
|
52
75
|
? boundedEntries.map(e => JSON.stringify(e)).join('\n') + '\n'
|
|
53
76
|
: '';
|
|
54
77
|
atomicWrite(file, content);
|
|
55
78
|
}
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
79
|
+
function trimFinalEntries(entries) {
|
|
80
|
+
const finalEntries = entries.filter(entry => !!entry.terminal || !!entry.deliveryResult);
|
|
81
|
+
if (finalEntries.length <= MAX_TERMINAL_ENTRIES_PER_AID)
|
|
59
82
|
return entries;
|
|
60
|
-
const
|
|
61
|
-
.sort((left, right) => (left.terminal?.at ?? left.ts) - (right.terminal?.at ?? right.ts))
|
|
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))
|
|
62
85
|
.slice(-MAX_TERMINAL_ENTRIES_PER_AID)
|
|
63
86
|
.map(entry => entry.id));
|
|
64
|
-
return entries.filter(entry => !entry.terminal ||
|
|
87
|
+
return entries.filter(entry => (!entry.terminal && !entry.deliveryResult) || retainedFinalIds.has(entry.id));
|
|
65
88
|
}
|
|
66
89
|
export function enqueue(aid, opts) {
|
|
90
|
+
const queue = opts.queue ?? 'default';
|
|
91
|
+
const config = queueConfig(queue);
|
|
67
92
|
const delivery = opts.delivery;
|
|
68
93
|
if (!isDeliveryForChannel(delivery, opts.channelId)) {
|
|
69
94
|
const code = isDeliveryTarget(delivery) ? 'AUN_OUTBOUND_ROUTE_MISMATCH' : 'AUN_OUTBOUND_ROUTE_REQUIRED';
|
|
70
95
|
throw Object.assign(new Error(`invalid AUN outbox delivery route for channelId=${opts.channelId}`), { code });
|
|
71
96
|
}
|
|
72
|
-
let existingEntries = readEntries(aid);
|
|
97
|
+
let existingEntries = readEntries(aid, queue);
|
|
98
|
+
for (const existing of existingEntries) {
|
|
99
|
+
if (isExpired(existing))
|
|
100
|
+
markExpired(existing);
|
|
101
|
+
}
|
|
73
102
|
if (opts.dedupeKey) {
|
|
74
103
|
// Terminal results are decisions for this logical operation. Reusing the
|
|
75
104
|
// same route must not silently turn a permanent rejection into a retry;
|
|
76
105
|
// an explicit route correction is the only case that replaces it.
|
|
77
|
-
const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey
|
|
106
|
+
const existing = existingEntries.find(entry => entry.dedupeKey === opts.dedupeKey
|
|
107
|
+
&& !entry.terminal
|
|
108
|
+
&& !entry.deliveryResult
|
|
109
|
+
&& !isExpired(entry));
|
|
78
110
|
if (existing
|
|
79
111
|
&& existing.channelId === opts.channelId
|
|
80
112
|
&& isDeliveryForChannel(existing.delivery, existing.channelId)
|
|
@@ -91,6 +123,7 @@ export function enqueue(aid, opts) {
|
|
|
91
123
|
id: generateId(),
|
|
92
124
|
ts: Date.now(),
|
|
93
125
|
aid,
|
|
126
|
+
...(queue === 'activity' ? { queue } : {}),
|
|
94
127
|
channelId: opts.channelId,
|
|
95
128
|
delivery,
|
|
96
129
|
dedupeKey: opts.dedupeKey,
|
|
@@ -103,103 +136,145 @@ export function enqueue(aid, opts) {
|
|
|
103
136
|
image: opts.image,
|
|
104
137
|
logText: opts.logText,
|
|
105
138
|
context: opts.context,
|
|
106
|
-
ttl: opts.ttl ??
|
|
139
|
+
ttl: opts.ttl ?? config.defaultTtl,
|
|
107
140
|
postSend: opts.postSend,
|
|
108
141
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let entries = existingEntries;
|
|
113
|
-
if (entries.filter(candidate => !candidate.critical && !candidate.terminal).length >= MAX_ENTRIES_PER_AID) {
|
|
114
|
-
const dropIndex = entries.findIndex(candidate => !candidate.critical && !candidate.terminal);
|
|
115
|
-
if (dropIndex >= 0)
|
|
116
|
-
entries.splice(dropIndex, 1);
|
|
117
|
-
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 });
|
|
118
145
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
writeEntries(aid, entries);
|
|
146
|
+
const entries = [...existingEntries, entry];
|
|
147
|
+
writeEntries(aid, entries, queue);
|
|
123
148
|
return entry;
|
|
124
149
|
}
|
|
125
|
-
export function remove(aid, id) {
|
|
126
|
-
const entries = readEntries(aid).filter(e => e.id !== id);
|
|
127
|
-
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);
|
|
128
153
|
}
|
|
129
154
|
/** Remove an entry only if it still points at the route that was submitted. */
|
|
130
|
-
export function removeIfRouteMatches(aid, submitted) {
|
|
131
|
-
const entries = readEntries(aid);
|
|
155
|
+
export function removeIfRouteMatches(aid, submitted, queue = entryQueue(submitted)) {
|
|
156
|
+
const entries = readEntries(aid, queue);
|
|
132
157
|
const index = entries.findIndex(entry => entry.id === submitted.id);
|
|
133
158
|
if (index < 0 || !hasSamePersistedRoute(entries[index], submitted))
|
|
134
159
|
return false;
|
|
135
160
|
entries.splice(index, 1);
|
|
136
|
-
writeEntries(aid, entries);
|
|
161
|
+
writeEntries(aid, entries, queue);
|
|
137
162
|
return true;
|
|
138
163
|
}
|
|
139
164
|
/** Replace an existing entry without consuming another outbox slot. */
|
|
140
165
|
export function replace(aid, entry) {
|
|
141
|
-
const
|
|
166
|
+
const queue = entryQueue(entry);
|
|
167
|
+
const entries = readEntries(aid, queue);
|
|
142
168
|
const index = entries.findIndex(candidate => candidate.id === entry.id);
|
|
143
169
|
if (index < 0)
|
|
144
170
|
return false;
|
|
145
171
|
entries[index] = entry;
|
|
146
|
-
writeEntries(aid, entries);
|
|
172
|
+
writeEntries(aid, entries, queue);
|
|
147
173
|
return true;
|
|
148
174
|
}
|
|
149
175
|
/** Replace an entry only while its submitted route is still current. */
|
|
150
|
-
export function replaceIfRouteMatches(aid, submitted, replacement) {
|
|
151
|
-
const entries = readEntries(aid);
|
|
176
|
+
export function replaceIfRouteMatches(aid, submitted, replacement, queue = entryQueue(submitted)) {
|
|
177
|
+
const entries = readEntries(aid, queue);
|
|
152
178
|
const index = entries.findIndex(candidate => candidate.id === submitted.id);
|
|
153
179
|
if (index < 0)
|
|
154
180
|
return 'missing';
|
|
155
181
|
if (!hasSamePersistedRoute(entries[index], submitted))
|
|
156
182
|
return 'route-changed';
|
|
157
183
|
entries[index] = replacement;
|
|
158
|
-
writeEntries(aid, entries);
|
|
184
|
+
writeEntries(aid, entries, queue);
|
|
159
185
|
return 'replaced';
|
|
160
186
|
}
|
|
161
187
|
/** Update only the delivery receipt while preserving concurrent entry changes. */
|
|
162
|
-
export function updateDeliveryReceiptIfRouteMatches(aid, submitted, receipt) {
|
|
163
|
-
const entries = readEntries(aid);
|
|
188
|
+
export function updateDeliveryReceiptIfRouteMatches(aid, submitted, receipt, queue = entryQueue(submitted)) {
|
|
189
|
+
const entries = readEntries(aid, queue);
|
|
164
190
|
const index = entries.findIndex(candidate => candidate.id === submitted.id);
|
|
165
191
|
if (index < 0)
|
|
166
192
|
return 'missing';
|
|
167
193
|
if (!hasSamePersistedRoute(entries[index], submitted))
|
|
168
194
|
return 'route-changed';
|
|
169
195
|
entries[index].deliveryReceipt = receipt;
|
|
170
|
-
writeEntries(aid, entries);
|
|
196
|
+
writeEntries(aid, entries, queue);
|
|
171
197
|
return 'replaced';
|
|
172
198
|
}
|
|
173
199
|
/** Remove durable interaction cards that were cancelled before delivery. */
|
|
174
|
-
export function removeInteractionCards(aid, requestId) {
|
|
175
|
-
const entries = readEntries(aid);
|
|
200
|
+
export function removeInteractionCards(aid, requestId, queue = 'default') {
|
|
201
|
+
const entries = readEntries(aid, queue);
|
|
176
202
|
const retained = entries.filter(entry => !(entry.postSend?.type === 'register_interaction_card'
|
|
177
203
|
&& entry.postSend.requestId === requestId));
|
|
178
204
|
const removed = entries.length - retained.length;
|
|
179
205
|
if (removed > 0)
|
|
180
|
-
writeEntries(aid, retained);
|
|
206
|
+
writeEntries(aid, retained, queue);
|
|
181
207
|
return removed;
|
|
182
208
|
}
|
|
183
|
-
export function load(aid) {
|
|
184
|
-
return readEntries(aid).filter(e => !e.terminal && !isExpired(e));
|
|
209
|
+
export function load(aid, queue = 'default') {
|
|
210
|
+
return readEntries(aid, queue).filter(e => !e.terminal && !e.deliveryResult && !isExpired(e));
|
|
185
211
|
}
|
|
186
|
-
export function findByDedupeKey(aid, dedupeKey, opts = {}) {
|
|
212
|
+
export function findByDedupeKey(aid, dedupeKey, opts = {}, queue = 'default') {
|
|
187
213
|
const entries = opts.includeTerminal
|
|
188
|
-
? readEntries(aid).filter(entry => !isExpired(entry))
|
|
189
|
-
: load(aid);
|
|
214
|
+
? readEntries(aid, queue).filter(entry => !isExpired(entry))
|
|
215
|
+
: load(aid, queue);
|
|
190
216
|
return entries.find(entry => entry.dedupeKey === dedupeKey);
|
|
191
217
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
/** Find all durable entries belonging to an operation family. */
|
|
219
|
+
export function findByDedupePrefix(aid, dedupePrefix, opts = {}, queue = 'default') {
|
|
220
|
+
const entries = opts.includeTerminal
|
|
221
|
+
? readEntries(aid, queue).filter(entry => !isExpired(entry))
|
|
222
|
+
: load(aid, queue);
|
|
223
|
+
return entries.filter(entry => typeof entry.dedupeKey === 'string' && entry.dedupeKey.startsWith(dedupePrefix));
|
|
224
|
+
}
|
|
225
|
+
export function cleanup(aid, queue = 'default') {
|
|
226
|
+
const all = readEntries(aid, queue);
|
|
227
|
+
let expired = 0;
|
|
228
|
+
for (const entry of all) {
|
|
229
|
+
if (!isExpired(entry))
|
|
230
|
+
continue;
|
|
231
|
+
expired++;
|
|
232
|
+
markExpired(entry);
|
|
233
|
+
}
|
|
234
|
+
if (expired > 0)
|
|
235
|
+
writeEntries(aid, all, queue);
|
|
236
|
+
return expired;
|
|
237
|
+
}
|
|
238
|
+
/** Resolve a durable delivery without treating disappearance as success. */
|
|
239
|
+
export function deliveryState(aid, id, queue = 'default') {
|
|
240
|
+
const entries = readEntries(aid, queue);
|
|
241
|
+
const entry = entries.find(candidate => candidate.id === id);
|
|
242
|
+
if (!entry)
|
|
243
|
+
return { status: 'missing' };
|
|
244
|
+
const deliveredMessageId = entry.deliveryResult?.messageId ?? entry.deliveryReceipt?.messageId;
|
|
245
|
+
if (deliveredMessageId) {
|
|
246
|
+
return { status: 'sent', messageId: deliveredMessageId };
|
|
247
|
+
}
|
|
248
|
+
if (entry.terminal) {
|
|
249
|
+
return {
|
|
250
|
+
status: 'failed',
|
|
251
|
+
error: entry.terminal.error,
|
|
252
|
+
...(entry.terminal.code !== undefined ? { code: entry.terminal.code } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (isExpired(entry)) {
|
|
256
|
+
markExpired(entry);
|
|
257
|
+
writeEntries(aid, entries, queue);
|
|
258
|
+
return {
|
|
259
|
+
status: 'failed',
|
|
260
|
+
error: entry.terminal.error,
|
|
261
|
+
code: entry.terminal.code,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { status: 'queued' };
|
|
265
|
+
}
|
|
266
|
+
function markExpired(entry) {
|
|
267
|
+
entry.lastError = 'outbox delivery expired';
|
|
268
|
+
entry.lastErrorCode = 'OUTBOX_EXPIRED';
|
|
269
|
+
entry.terminal = {
|
|
270
|
+
at: Date.now(),
|
|
271
|
+
error: entry.lastError,
|
|
272
|
+
code: entry.lastErrorCode,
|
|
273
|
+
};
|
|
199
274
|
}
|
|
200
275
|
/** Mark a durable entry terminal while keeping its reason for diagnostics. */
|
|
201
|
-
export function markTerminal(aid, id, failure, submitted) {
|
|
202
|
-
const entries = readEntries(aid);
|
|
276
|
+
export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
|
|
277
|
+
const entries = readEntries(aid, queue);
|
|
203
278
|
const entry = entries.find(candidate => candidate.id === id);
|
|
204
279
|
if (!entry || (submitted && !hasSamePersistedRoute(entry, submitted)))
|
|
205
280
|
return false;
|
|
@@ -207,7 +282,20 @@ export function markTerminal(aid, id, failure, submitted) {
|
|
|
207
282
|
if (failure.code !== undefined)
|
|
208
283
|
entry.lastErrorCode = failure.code;
|
|
209
284
|
entry.terminal = { at: Date.now(), error: entry.lastError, ...(failure.code !== undefined ? { code: failure.code } : {}) };
|
|
210
|
-
writeEntries(aid, entries);
|
|
285
|
+
writeEntries(aid, entries, queue);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
/** Retain a bounded success receipt for queued Trigger history reconciliation. */
|
|
289
|
+
export function markDelivered(aid, id, messageId, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
|
|
290
|
+
const entries = readEntries(aid, queue);
|
|
291
|
+
const entry = entries.find(candidate => candidate.id === id);
|
|
292
|
+
if (!entry || (submitted && !hasSamePersistedRoute(entry, submitted)))
|
|
293
|
+
return false;
|
|
294
|
+
entry.deliveryResult = { status: 'sent', at: Date.now(), messageId };
|
|
295
|
+
delete entry.lastError;
|
|
296
|
+
delete entry.lastErrorCode;
|
|
297
|
+
delete entry.terminal;
|
|
298
|
+
writeEntries(aid, entries, queue);
|
|
211
299
|
return true;
|
|
212
300
|
}
|
|
213
301
|
// Each caller gets its own pass over the current disk state. This prevents
|
|
@@ -226,8 +314,8 @@ function hasSamePersistedRoute(left, right) {
|
|
|
226
314
|
&& isDeliveryTarget(right.delivery)
|
|
227
315
|
&& sameDeliveryTarget(left.delivery, right.delivery);
|
|
228
316
|
}
|
|
229
|
-
async function drainOnce(aid, sender) {
|
|
230
|
-
const entries = readEntries(aid).filter(entry => !entry.terminal);
|
|
317
|
+
async function drainOnce(aid, sender, queue) {
|
|
318
|
+
const entries = readEntries(aid, queue).filter(entry => !entry.terminal && !entry.deliveryResult);
|
|
231
319
|
if (entries.length === 0)
|
|
232
320
|
return { sent: 0, expired: 0, failed: 0 };
|
|
233
321
|
const drainedIds = new Set(entries.map(e => e.id));
|
|
@@ -240,6 +328,10 @@ async function drainOnce(aid, sender) {
|
|
|
240
328
|
for (const entry of entries) {
|
|
241
329
|
if (isExpired(entry)) {
|
|
242
330
|
expired++;
|
|
331
|
+
markExpired(entry);
|
|
332
|
+
// Keep the terminal record so Trigger history can distinguish an
|
|
333
|
+
// operation that expired from one that was actually sent.
|
|
334
|
+
remaining.push(entry);
|
|
243
335
|
continue;
|
|
244
336
|
}
|
|
245
337
|
if (!hasRouteForChannel(entry)) {
|
|
@@ -265,7 +357,23 @@ async function drainOnce(aid, sender) {
|
|
|
265
357
|
? { status: raw ? 'sent' : 'retry' }
|
|
266
358
|
: raw;
|
|
267
359
|
if (result.status === 'sent') {
|
|
268
|
-
|
|
360
|
+
const messageId = result.messageId ?? entry.deliveryReceipt?.messageId;
|
|
361
|
+
if (messageId) {
|
|
362
|
+
sent++;
|
|
363
|
+
entry.deliveryResult = { status: 'sent', at: Date.now(), messageId };
|
|
364
|
+
remaining.push(entry);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
permanent++;
|
|
368
|
+
entry.lastError = 'outbox sender returned sent without a remote message_id';
|
|
369
|
+
entry.lastErrorCode = 'MISSING_MESSAGE_ID';
|
|
370
|
+
entry.terminal = {
|
|
371
|
+
at: Date.now(),
|
|
372
|
+
error: entry.lastError,
|
|
373
|
+
code: entry.lastErrorCode,
|
|
374
|
+
};
|
|
375
|
+
remaining.push(entry);
|
|
376
|
+
}
|
|
269
377
|
}
|
|
270
378
|
else if (result.status === 'permanent') {
|
|
271
379
|
permanent++;
|
|
@@ -295,7 +403,7 @@ async function drainOnce(aid, sender) {
|
|
|
295
403
|
remaining.push(entry);
|
|
296
404
|
}
|
|
297
405
|
}
|
|
298
|
-
const current = readEntries(aid);
|
|
406
|
+
const current = readEntries(aid, queue);
|
|
299
407
|
const failedById = new Map(remaining.map(entry => [entry.id, entry]));
|
|
300
408
|
const retained = current.flatMap(entry => {
|
|
301
409
|
if (!drainedIds.has(entry.id))
|
|
@@ -324,34 +432,35 @@ async function drainOnce(aid, sender) {
|
|
|
324
432
|
...(failedEntry.terminal ? { terminal: failedEntry.terminal } : {}),
|
|
325
433
|
}];
|
|
326
434
|
});
|
|
327
|
-
writeEntries(aid, retained);
|
|
435
|
+
writeEntries(aid, retained, queue);
|
|
328
436
|
return { sent, expired, failed, ...(permanent > 0 ? { permanent } : {}) };
|
|
329
437
|
}
|
|
330
|
-
export async function drain(aid, sender) {
|
|
331
|
-
const
|
|
438
|
+
export async function drain(aid, sender, queue = 'default') {
|
|
439
|
+
const drainKey = `${queue}:${aid}`;
|
|
440
|
+
const previous = activeDrainTails.get(drainKey);
|
|
332
441
|
let releaseTurn;
|
|
333
442
|
const turn = new Promise(resolve => { releaseTurn = resolve; });
|
|
334
443
|
const tail = (previous ?? Promise.resolve()).then(() => turn);
|
|
335
|
-
activeDrainTails.set(
|
|
444
|
+
activeDrainTails.set(drainKey, tail);
|
|
336
445
|
if (previous)
|
|
337
446
|
await previous;
|
|
338
447
|
try {
|
|
339
|
-
return await drainOnce(aid, sender);
|
|
448
|
+
return await drainOnce(aid, sender, queue);
|
|
340
449
|
}
|
|
341
450
|
finally {
|
|
342
451
|
releaseTurn();
|
|
343
|
-
if (activeDrainTails.get(
|
|
344
|
-
activeDrainTails.delete(
|
|
452
|
+
if (activeDrainTails.get(drainKey) === tail)
|
|
453
|
+
activeDrainTails.delete(drainKey);
|
|
345
454
|
}
|
|
346
455
|
}
|
|
347
|
-
export function hasPending(aid) {
|
|
348
|
-
return readEntries(aid).some(entry => !entry.terminal && !isExpired(entry));
|
|
456
|
+
export function hasPending(aid, queue = 'default') {
|
|
457
|
+
return readEntries(aid, queue).some(entry => !entry.terminal && !entry.deliveryResult && !isExpired(entry));
|
|
349
458
|
}
|
|
350
459
|
/**
|
|
351
460
|
* 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
|
|
352
461
|
* message.send 出队速度跟不上入队速度(网关慢 / 链路抖动 / 卡片洪泛),
|
|
353
462
|
* 是命令「回复慢/无回复」的先行指标。
|
|
354
463
|
*/
|
|
355
|
-
export function pendingCount(aid) {
|
|
356
|
-
return load(aid).length;
|
|
464
|
+
export function pendingCount(aid, queue = 'default') {
|
|
465
|
+
return load(aid, queue).length;
|
|
357
466
|
}
|
|
@@ -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}`);
|