evolcore 0.0.20 → 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.
Files changed (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -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 MAX_ENTRIES_PER_AID = 20;
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 DEFAULT_TTL = 300_000; // 5 minutes
8
- function outboxFile(aid) {
9
- return agentOutboxPath(aid);
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 = trimTerminalEntries(entries);
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 trimTerminalEntries(entries) {
57
- const terminalEntries = entries.filter(entry => !!entry.terminal);
58
- if (terminalEntries.length <= MAX_TERMINAL_ENTRIES_PER_AID)
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 retainedTerminalIds = new Set(terminalEntries
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 || retainedTerminalIds.has(entry.id));
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 && !isExpired(entry));
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,138 @@ export function enqueue(aid, opts) {
103
136
  image: opts.image,
104
137
  logText: opts.logText,
105
138
  context: opts.context,
106
- ttl: opts.ttl ?? DEFAULT_TTL,
139
+ ttl: opts.ttl ?? config.defaultTtl,
107
140
  postSend: opts.postSend,
108
141
  };
109
- // Enforce the pending-entry cap without allowing historical terminal
110
- // diagnostics to evict live messages. Terminal records are bounded
111
- // separately so a permanently failing target cannot grow the file forever.
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];
118
- }
119
- else {
120
- 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 });
121
145
  }
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 entries = readEntries(aid);
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
- export function cleanup(aid) {
193
- const all = readEntries(aid);
194
- const valid = all.filter(e => !isExpired(e));
195
- const removed = all.length - valid.length;
196
- if (removed > 0)
197
- writeEntries(aid, valid);
198
- return removed;
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
+ };
199
267
  }
200
268
  /** Mark a durable entry terminal while keeping its reason for diagnostics. */
201
- export function markTerminal(aid, id, failure, submitted) {
202
- const entries = readEntries(aid);
269
+ export function markTerminal(aid, id, failure, submitted, queue = submitted ? entryQueue(submitted) : 'default') {
270
+ const entries = readEntries(aid, queue);
203
271
  const entry = entries.find(candidate => candidate.id === id);
204
272
  if (!entry || (submitted && !hasSamePersistedRoute(entry, submitted)))
205
273
  return false;
@@ -207,7 +275,20 @@ export function markTerminal(aid, id, failure, submitted) {
207
275
  if (failure.code !== undefined)
208
276
  entry.lastErrorCode = failure.code;
209
277
  entry.terminal = { at: Date.now(), error: entry.lastError, ...(failure.code !== undefined ? { code: failure.code } : {}) };
210
- writeEntries(aid, entries);
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);
211
292
  return true;
212
293
  }
213
294
  // Each caller gets its own pass over the current disk state. This prevents
@@ -226,8 +307,8 @@ function hasSamePersistedRoute(left, right) {
226
307
  && isDeliveryTarget(right.delivery)
227
308
  && sameDeliveryTarget(left.delivery, right.delivery);
228
309
  }
229
- async function drainOnce(aid, sender) {
230
- const entries = readEntries(aid).filter(entry => !entry.terminal);
310
+ async function drainOnce(aid, sender, queue) {
311
+ const entries = readEntries(aid, queue).filter(entry => !entry.terminal && !entry.deliveryResult);
231
312
  if (entries.length === 0)
232
313
  return { sent: 0, expired: 0, failed: 0 };
233
314
  const drainedIds = new Set(entries.map(e => e.id));
@@ -240,6 +321,10 @@ async function drainOnce(aid, sender) {
240
321
  for (const entry of entries) {
241
322
  if (isExpired(entry)) {
242
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);
243
328
  continue;
244
329
  }
245
330
  if (!hasRouteForChannel(entry)) {
@@ -265,7 +350,23 @@ async function drainOnce(aid, sender) {
265
350
  ? { status: raw ? 'sent' : 'retry' }
266
351
  : raw;
267
352
  if (result.status === 'sent') {
268
- 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
+ }
269
370
  }
270
371
  else if (result.status === 'permanent') {
271
372
  permanent++;
@@ -295,7 +396,7 @@ async function drainOnce(aid, sender) {
295
396
  remaining.push(entry);
296
397
  }
297
398
  }
298
- const current = readEntries(aid);
399
+ const current = readEntries(aid, queue);
299
400
  const failedById = new Map(remaining.map(entry => [entry.id, entry]));
300
401
  const retained = current.flatMap(entry => {
301
402
  if (!drainedIds.has(entry.id))
@@ -324,34 +425,35 @@ async function drainOnce(aid, sender) {
324
425
  ...(failedEntry.terminal ? { terminal: failedEntry.terminal } : {}),
325
426
  }];
326
427
  });
327
- writeEntries(aid, retained);
428
+ writeEntries(aid, retained, queue);
328
429
  return { sent, expired, failed, ...(permanent > 0 ? { permanent } : {}) };
329
430
  }
330
- export async function drain(aid, sender) {
331
- const previous = activeDrainTails.get(aid);
431
+ export async function drain(aid, sender, queue = 'default') {
432
+ const drainKey = `${queue}:${aid}`;
433
+ const previous = activeDrainTails.get(drainKey);
332
434
  let releaseTurn;
333
435
  const turn = new Promise(resolve => { releaseTurn = resolve; });
334
436
  const tail = (previous ?? Promise.resolve()).then(() => turn);
335
- activeDrainTails.set(aid, tail);
437
+ activeDrainTails.set(drainKey, tail);
336
438
  if (previous)
337
439
  await previous;
338
440
  try {
339
- return await drainOnce(aid, sender);
441
+ return await drainOnce(aid, sender, queue);
340
442
  }
341
443
  finally {
342
444
  releaseTurn();
343
- if (activeDrainTails.get(aid) === tail)
344
- activeDrainTails.delete(aid);
445
+ if (activeDrainTails.get(drainKey) === tail)
446
+ activeDrainTails.delete(drainKey);
345
447
  }
346
448
  }
347
- export function hasPending(aid) {
348
- return readEntries(aid).some(entry => !entry.terminal && !isExpired(entry));
449
+ export function hasPending(aid, queue = 'default') {
450
+ return readEntries(aid, queue).some(entry => !entry.terminal && !entry.deliveryResult && !isExpired(entry));
349
451
  }
350
452
  /**
351
453
  * 当前 outbox 中待发送条目数。用于诊断发送管线堵塞:depth 持续增长说明
352
454
  * message.send 出队速度跟不上入队速度(网关慢 / 链路抖动 / 卡片洪泛),
353
455
  * 是命令「回复慢/无回复」的先行指标。
354
456
  */
355
- export function pendingCount(aid) {
356
- return load(aid).length;
457
+ export function pendingCount(aid, queue = 'default') {
458
+ return load(aid, queue).length;
357
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='ecweb' 时读 data/instance/ecweb-*.json port
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 ECWEB_DISCOVERY_TIMEOUT_MS = 10_000;
24
- const ECWEB_DISCOVERY_INTERVAL_MS = 250;
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/ 下存活的 ecweb 实例端口。
29
+ * 读 data/instance/ 下指定服务的存活实例端口。
30
30
  * 取 startedAt 最新的一条(多实例保护下通常只有一条存活)。
31
31
  */
32
- function discoverEcwebPort() {
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
- if (!/^(ecweb|watch-web)-\d+\.json$/.test(file))
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 && typeof rec.pid === 'number' && isProcessAlive(rec.pid) && typeof rec.port === 'number') {
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 waitForEcwebPort(serviceName) {
71
- const deadline = Date.now() + ECWEB_DISCOVERY_TIMEOUT_MS;
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 = discoverEcwebPort();
88
+ const port = discoverInstancePort(serviceName);
75
89
  if (port)
76
90
  return port;
77
91
  if (!loggedWait) {
78
- logger.info(`${LOG} 服务 "${serviceName}" source=ecweb,等待 ecweb 实例就绪`);
92
+ logger.info(`${LOG} 服务 "${serviceName}" source=instance,等待实例就绪`);
79
93
  loggedWait = true;
80
94
  }
81
- await sleep(ECWEB_DISCOVERY_INTERVAL_MS);
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 === 'ecweb') {
90
- const port = await waitForEcwebPort(svc.name);
103
+ if (svc.proxy?.source === 'instance') {
104
+ const port = await waitForInstancePort(svc.name);
91
105
  if (!port) {
92
- logger.warn(`${LOG} 服务 "${svc.name}" source=ecweb 等待 ${ECWEB_DISCOVERY_TIMEOUT_MS}ms 后仍未发现存活的 ecweb 实例,跳过`);
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
- // static
98
- if (svc.endpoint && svc.endpoint.trim())
99
- return svc.endpoint.trim();
100
- logger.warn(`${LOG} 服务 "${svc.name}" source=static 但未配置 endpoint,跳过`);
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, config) {
109
- const services = (config.services ?? []).filter((s) => s.enabled !== false);
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}`);