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.
Files changed (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. 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,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 ?? 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];
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
- else {
120
- entries = [...entries, entry];
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 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
+ /** 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
- sent++;
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 previous = activeDrainTails.get(aid);
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(aid, tail);
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(aid) === tail)
344
- activeDrainTails.delete(aid);
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='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}`);