@evomap/evolver 1.89.4 → 1.89.5

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 (105) hide show
  1. package/CONTRIBUTING.md +19 -0
  2. package/README.md +536 -86
  3. package/assets/cover.png +0 -0
  4. package/index.js +87 -7
  5. package/package.json +17 -6
  6. package/scripts/a2a_export.js +63 -0
  7. package/scripts/a2a_ingest.js +79 -0
  8. package/scripts/a2a_promote.js +118 -0
  9. package/scripts/analyze_by_skill.js +121 -0
  10. package/scripts/build_binaries.js +479 -0
  11. package/scripts/check-changelog.js +166 -0
  12. package/scripts/extract_log.js +85 -0
  13. package/scripts/generate_history.js +75 -0
  14. package/scripts/gep_append_event.js +96 -0
  15. package/scripts/gep_personality_report.js +234 -0
  16. package/scripts/human_report.js +147 -0
  17. package/scripts/recall-verify-report.js +234 -0
  18. package/scripts/recover_loop.js +61 -0
  19. package/scripts/refresh_stars_badge.js +168 -0
  20. package/scripts/seed-merchants.js +91 -0
  21. package/scripts/suggest_version.js +89 -0
  22. package/scripts/validate-modules.js +38 -0
  23. package/scripts/validate-suite.js +78 -0
  24. package/skills/index.json +14 -0
  25. package/src/adapters/scripts/_runtimePaths.js +1 -0
  26. package/src/adapters/scripts/evolver-session-end.js +1 -0
  27. package/src/adapters/scripts/evolver-session-start.js +1 -0
  28. package/src/evolve/guards.js +1 -721
  29. package/src/evolve/pipeline/collect.js +1 -1283
  30. package/src/evolve/pipeline/dispatch.js +1 -421
  31. package/src/evolve/pipeline/enrich.js +1 -440
  32. package/src/evolve/pipeline/hub.js +1 -319
  33. package/src/evolve/pipeline/select.js +1 -274
  34. package/src/evolve/pipeline/signals.js +1 -206
  35. package/src/evolve/utils.js +1 -264
  36. package/src/evolve.js +1 -350
  37. package/src/gep/a2aProtocol.js +1 -4455
  38. package/src/gep/antiAbuseTelemetry.js +1 -233
  39. package/src/gep/autoDistillConv.js +1 -205
  40. package/src/gep/autoDistillLlm.js +1 -315
  41. package/src/gep/candidateEval.js +1 -92
  42. package/src/gep/candidates.js +1 -198
  43. package/src/gep/contentHash.js +1 -30
  44. package/src/gep/conversationSniffer.js +1 -266
  45. package/src/gep/crypto.js +1 -89
  46. package/src/gep/curriculum.js +1 -163
  47. package/src/gep/deviceId.js +1 -218
  48. package/src/gep/envFingerprint.js +1 -118
  49. package/src/gep/epigenetics.js +1 -31
  50. package/src/gep/execBridge.js +1 -711
  51. package/src/gep/explore.js +1 -289
  52. package/src/gep/hash.js +1 -15
  53. package/src/gep/hubFetch.js +1 -359
  54. package/src/gep/hubReview.js +1 -207
  55. package/src/gep/hubSearch.js +1 -526
  56. package/src/gep/hubVerify.js +1 -306
  57. package/src/gep/idleScheduler.js +6 -1
  58. package/src/gep/learningSignals.js +1 -89
  59. package/src/gep/memoryGraph.js +1 -1374
  60. package/src/gep/memoryGraphAdapter.js +1 -203
  61. package/src/gep/mutation.js +1 -203
  62. package/src/gep/narrativeMemory.js +1 -108
  63. package/src/gep/openPRRegistry.js +1 -205
  64. package/src/gep/personality.js +1 -423
  65. package/src/gep/policyCheck.js +1 -599
  66. package/src/gep/prompt.js +1 -836
  67. package/src/gep/recallInject.js +1 -409
  68. package/src/gep/recallVerifier.js +1 -318
  69. package/src/gep/reflection.js +1 -177
  70. package/src/gep/selector.js +1 -602
  71. package/src/gep/skillDistiller.js +1 -1294
  72. package/src/gep/solidify.js +1 -1699
  73. package/src/gep/strategy.js +1 -136
  74. package/src/gep/tokenSavings.js +1 -88
  75. package/src/gep/workspaceKeychain.js +1 -174
  76. package/src/ops/lifecycle.js +17 -4
  77. package/src/proxy/extensions/traceControl.js +1 -99
  78. package/src/proxy/index.js +206 -1
  79. package/src/proxy/inject.js +1 -52
  80. package/src/proxy/lifecycle/manager.js +12 -0
  81. package/src/proxy/mailbox/store.js +29 -6
  82. package/src/proxy/router/responses_route.js +157 -0
  83. package/src/proxy/server/http.js +13 -4
  84. package/src/proxy/server/routes.js +11 -1
  85. package/src/proxy/sync/engine.js +7 -1
  86. package/src/proxy/sync/outbound.js +32 -4
  87. package/src/proxy/trace/extractor.js +1 -646
  88. package/src/proxy/trace/usage.js +1 -105
  89. package/.cursor/BUGBOT.md +0 -182
  90. package/.env.example +0 -68
  91. package/.git-commit-guard-token +0 -1
  92. package/.github/CODEOWNERS +0 -63
  93. package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -23
  94. package/.github/pull_request_template.md +0 -45
  95. package/.github/workflows/test.yml +0 -75
  96. package/CHANGELOG.md +0 -1237
  97. package/README.public.md +0 -569
  98. package/SECURITY.md +0 -108
  99. package/assets/gep/events.jsonl +0 -3
  100. package/examples/atp-consumer-quickstart.md +0 -100
  101. package/examples/hello-world.md +0 -38
  102. package/proxy-package.json +0 -39
  103. package/public.manifest.json +0 -143
  104. /package/assets/gep/{genes.json → genes.seed.json} +0 -0
  105. /package/{bundled-skills → skills}/_meta/SKILL.md +0 -0
@@ -7,6 +7,24 @@ const crypto = require('crypto');
7
7
  const DEFAULT_CHANNEL = 'evomap-hub';
8
8
  const SCHEMA_VERSION = 1;
9
9
  const PROXY_PROTOCOL_VERSION = '0.1.0';
10
+ const PRIVATE_DIR_MODE = 0o700;
11
+ const PRIVATE_FILE_MODE = 0o600;
12
+
13
+ function bestEffortChmod(filePath, mode) {
14
+ try { fs.chmodSync(filePath, mode); } catch { /* best effort; no-op on Windows */ }
15
+ }
16
+
17
+ function ensurePrivateDir(dir) {
18
+ if (!fs.existsSync(dir)) {
19
+ fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
20
+ }
21
+ bestEffortChmod(dir, PRIVATE_DIR_MODE);
22
+ }
23
+
24
+ function writePrivateFile(filePath, content) {
25
+ fs.writeFileSync(filePath, content, { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
26
+ bestEffortChmod(filePath, PRIVATE_FILE_MODE);
27
+ }
10
28
 
11
29
  // Merge `fields` into `target` while stripping keys that can mutate the
12
30
  // prototype chain. Mailbox rows are persisted as JSONL and rebuilt on
@@ -90,8 +108,8 @@ function safeParse(payload) {
90
108
  // regular files that POSIX local filesystems do, so the removal above is
91
109
  // equally valid on Windows. No platform-specific code is needed here.
92
110
  function appendLine(filePath, obj) {
93
- fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
94
- try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
111
+ fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', { encoding: 'utf8', mode: PRIVATE_FILE_MODE });
112
+ bestEffortChmod(filePath, PRIVATE_FILE_MODE);
95
113
  }
96
114
 
97
115
  function readLines(filePath) {
@@ -112,11 +130,13 @@ function readLines(filePath) {
112
130
  class MailboxStore {
113
131
  constructor(dataDir) {
114
132
  if (!dataDir) throw new Error('dataDir is required');
115
- if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
133
+ ensurePrivateDir(dataDir);
116
134
  this.dataDir = dataDir;
117
135
 
118
136
  this._messagesFile = path.join(dataDir, 'messages.jsonl');
119
137
  this._stateFile = path.join(dataDir, 'state.json');
138
+ bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
139
+ bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
120
140
 
121
141
  // in-memory indexes
122
142
  this._messages = new Map(); // id -> message object
@@ -156,7 +176,7 @@ class MailboxStore {
156
176
 
157
177
  _persistState() {
158
178
  const dir = path.dirname(this._stateFile);
159
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
179
+ ensurePrivateDir(dir);
160
180
  // Round-7 (§20.5): per-PID tmp path. Two evolver processes (daemon +
161
181
  // ad-hoc CLI / proxy + loop) writing to the same `${stateFile}.tmp`
162
182
  // would otherwise interleave: process B's writeFileSync truncates
@@ -167,7 +187,7 @@ class MailboxStore {
167
187
  // for 30 min..4 h" symptom this branch targets. Matches the
168
188
  // precedent set by _persistNodeSecret in src/gep/a2aProtocol.js.
169
189
  const tmp = `${this._stateFile}.${process.pid}.tmp`;
170
- fs.writeFileSync(tmp, JSON.stringify(this._state, null, 2) + '\n', 'utf8');
190
+ writePrivateFile(tmp, JSON.stringify(this._state, null, 2) + '\n');
171
191
  // Windows: fs.renameSync throws EPERM when the destination file already
172
192
  // exists, unlike POSIX where rename(2) atomically replaces the target.
173
193
  // Remove the destination first so the rename succeeds on all platforms.
@@ -179,6 +199,7 @@ class MailboxStore {
179
199
  }
180
200
  }
181
201
  fs.renameSync(tmp, this._stateFile);
202
+ bestEffortChmod(this._stateFile, PRIVATE_FILE_MODE);
182
203
  }
183
204
 
184
205
  _rebuildIndex() {
@@ -436,11 +457,12 @@ class MailboxStore {
436
457
  }
437
458
  entries.sort((a, b) => a.created_at - b.created_at);
438
459
 
439
- const fd = fs.openSync(tmpFile, 'w');
460
+ const fd = fs.openSync(tmpFile, 'w', PRIVATE_FILE_MODE);
440
461
  for (const msg of entries) {
441
462
  fs.writeSync(fd, JSON.stringify(msg) + '\n');
442
463
  }
443
464
  fs.closeSync(fd);
465
+ bestEffortChmod(tmpFile, PRIVATE_FILE_MODE);
444
466
  // Windows: renameSync throws EPERM when the destination already exists.
445
467
  // Remove it first so the swap succeeds on all platforms.
446
468
  if (process.platform === 'win32') {
@@ -449,6 +471,7 @@ class MailboxStore {
449
471
  }
450
472
  }
451
473
  fs.renameSync(tmpFile, this._messagesFile);
474
+ bestEffortChmod(this._messagesFile, PRIVATE_FILE_MODE);
452
475
  this._rebuildIndex();
453
476
  }
454
477
 
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const { createProxyTrace } = require('../trace/extractor');
4
+
5
+ const OPENAI_RESPONSE_HEADER_ALLOWLIST = new Set([
6
+ 'openai-processing-ms',
7
+ 'openai-version',
8
+ 'retry-after',
9
+ 'x-request-id',
10
+ ]);
11
+
12
+ function hasOpenAIUpstreamCredential() {
13
+ if (process.env.EVOMAP_OPENAI_API_KEY || process.env.OPENAI_API_KEY) return true;
14
+ return false;
15
+ }
16
+
17
+ function upstreamStatus(err, fallback = 502) {
18
+ const status = Number(err && err.statusCode);
19
+ return Number.isFinite(status) ? status : fallback;
20
+ }
21
+
22
+ function safeOpenAIConfigDiagnostic(err) {
23
+ const message = err && typeof err.message === 'string' ? err.message : '';
24
+ if (message.startsWith('[proxy] EVOMAP_OPENAI_BASE_URL ')) return message;
25
+ return '';
26
+ }
27
+
28
+ function asUpstreamError(err, fallback = 502) {
29
+ if (err && err.statusCode && /^openai upstream /.test(err.message || '')) return err;
30
+ const diagnostic = safeOpenAIConfigDiagnostic(err);
31
+ const message = diagnostic
32
+ ? `openai upstream request failed: ${diagnostic}`
33
+ : 'openai upstream request failed';
34
+ const out = new Error(message);
35
+ out.statusCode = upstreamStatus(err, fallback);
36
+ out.cause = err;
37
+ return out;
38
+ }
39
+
40
+ function responseToBody(raw, status, headers, log) {
41
+ if (!raw) return {};
42
+ try {
43
+ return JSON.parse(raw);
44
+ } catch {
45
+ log.warn?.(JSON.stringify({
46
+ event: 'openai_responses_fallback',
47
+ reason: 'upstream_non_json',
48
+ upstream_status: status,
49
+ content_type: headers && headers['content-type'] || '',
50
+ response_bytes: Buffer.byteLength(raw),
51
+ }));
52
+ return { error: raw };
53
+ }
54
+ }
55
+
56
+ function copyOpenAIResponseHeaders(headers = {}) {
57
+ const out = {};
58
+ for (const [name, value] of Object.entries(headers || {})) {
59
+ const lower = String(name || '').toLowerCase();
60
+ if (!OPENAI_RESPONSE_HEADER_ALLOWLIST.has(lower) && !lower.startsWith('x-ratelimit-')) continue;
61
+ if (value === undefined || value === null) continue;
62
+ const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
63
+ if (/[\r\n]/.test(headerValue)) continue;
64
+ out[lower] = headerValue;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ function buildResponsesHandler({ openAIProxy, logger, traceStore, onTraceQueued } = {}) {
70
+ if (typeof openAIProxy !== 'function') {
71
+ throw new Error('buildResponsesHandler requires openAIProxy(path, body, opts)');
72
+ }
73
+ const log = logger || console;
74
+
75
+ return async ({ body, headers }) => {
76
+ const inboundHeaders = headers || {};
77
+ if (!hasOpenAIUpstreamCredential()) {
78
+ throw Object.assign(new Error('openai api key required'), { statusCode: 401 });
79
+ }
80
+
81
+ const originalModel = body && typeof body.model === 'string' ? body.model : null;
82
+ let trace = null;
83
+ try {
84
+ trace = createProxyTrace({
85
+ route: 'POST /v1/responses',
86
+ headers: inboundHeaders,
87
+ body,
88
+ upstreamMode: 'openai',
89
+ originalModel,
90
+ chosenModel: originalModel,
91
+ store: traceStore,
92
+ logger: traceStore ? log : null,
93
+ onTraceQueued,
94
+ });
95
+ } catch (_) { /* best-effort trace; never break the request */ }
96
+
97
+ let upstream;
98
+ try {
99
+ upstream = await openAIProxy('/responses', body, {
100
+ inboundHeaders,
101
+ upstreamMode: 'openai',
102
+ });
103
+ } catch (err) {
104
+ const wrapped = asUpstreamError(err, upstreamStatus(err));
105
+ trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'openai', model: originalModel });
106
+ throw wrapped;
107
+ }
108
+
109
+ if (upstream.stream) {
110
+ const forwardHeaders = copyOpenAIResponseHeaders(upstream.headers);
111
+ const ct = upstream.headers && upstream.headers['content-type'];
112
+ if (ct) forwardHeaders['Content-Type'] = ct;
113
+ trace?.recordStreamStart({
114
+ status: upstream.status,
115
+ upstreamMode: 'openai',
116
+ model: originalModel,
117
+ headers: forwardHeaders,
118
+ });
119
+ return {
120
+ status: upstream.status,
121
+ stream: upstream.stream,
122
+ headers: forwardHeaders,
123
+ };
124
+ }
125
+
126
+ let raw = '';
127
+ if (upstream.text) {
128
+ try {
129
+ raw = await upstream.text();
130
+ } catch (err) {
131
+ const wrapped = asUpstreamError(err, upstreamStatus(err));
132
+ trace?.record({ status: wrapped.statusCode, error: wrapped, upstreamMode: 'openai', model: originalModel });
133
+ throw wrapped;
134
+ }
135
+ }
136
+ const respBody = responseToBody(raw, upstream.status, upstream.headers, log);
137
+ trace?.record({
138
+ status: upstream.status,
139
+ responseBody: respBody,
140
+ upstreamMode: 'openai',
141
+ model: originalModel,
142
+ headers: upstream.headers,
143
+ });
144
+ return {
145
+ status: upstream.status,
146
+ body: respBody,
147
+ headers: copyOpenAIResponseHeaders(upstream.headers),
148
+ };
149
+ };
150
+ }
151
+
152
+ module.exports = {
153
+ buildResponsesHandler,
154
+ copyOpenAIResponseHeaders,
155
+ hasOpenAIUpstreamCredential,
156
+ responseToBody,
157
+ };
@@ -64,12 +64,21 @@ function parseBody(req, opts) {
64
64
  });
65
65
  }
66
66
 
67
- function sendJson(res, status, body) {
67
+ function sendJson(res, status, body, extraHeaders = {}) {
68
68
  const payload = JSON.stringify(body);
69
- res.writeHead(status, {
69
+ const headers = {
70
70
  'Content-Type': 'application/json',
71
71
  'Content-Length': Buffer.byteLength(payload),
72
- });
72
+ };
73
+ for (const [name, value] of Object.entries(extraHeaders || {})) {
74
+ if (value === undefined || value === null) continue;
75
+ const lower = String(name).toLowerCase();
76
+ if (lower === 'content-length' || lower === 'transfer-encoding' || lower === 'connection') continue;
77
+ const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
78
+ if (/[\r\n]/.test(headerValue)) continue;
79
+ headers[name] = headerValue;
80
+ }
81
+ res.writeHead(status, headers);
73
82
  res.end(payload);
74
83
  }
75
84
 
@@ -196,7 +205,7 @@ class ProxyHttpServer {
196
205
  if (result && result.stream) {
197
206
  await this._streamResponse(res, result);
198
207
  } else {
199
- sendJson(res, result.status || 200, result.body || result);
208
+ sendJson(res, result.status || 200, result.body || result, result.headers);
200
209
  }
201
210
  } catch (err) {
202
211
  this.logger.error(`[proxy] ${routeKey} error:`, err.message);
@@ -3,7 +3,14 @@
3
3
  const { PROXY_PROTOCOL_VERSION, SCHEMA_VERSION } = require('../mailbox/store');
4
4
 
5
5
  function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
6
- const { dmHandler, skillUpdater, getHubMailboxStatus, sessionHandler, messagesHandler } = extensions || {};
6
+ const {
7
+ dmHandler,
8
+ skillUpdater,
9
+ getHubMailboxStatus,
10
+ sessionHandler,
11
+ messagesHandler,
12
+ responsesHandler,
13
+ } = extensions || {};
7
14
  const routes = {
8
15
  // -- Mailbox --
9
16
  'POST /mailbox/send': async ({ body }) => {
@@ -466,6 +473,9 @@ function buildRoutes(store, proxyHandlers, taskMonitor, extensions) {
466
473
  if (messagesHandler) {
467
474
  routes['POST /v1/messages'] = messagesHandler;
468
475
  }
476
+ if (responsesHandler) {
477
+ routes['POST /v1/responses'] = responsesHandler;
478
+ }
469
479
 
470
480
  return routes;
471
481
  }
@@ -8,13 +8,14 @@ const DEFAULT_OUTBOUND_INTERVAL = 5_000;
8
8
  const IDLE_THRESHOLD = 5 * 60_000;
9
9
 
10
10
  class SyncEngine {
11
- constructor({ store, hubUrl, getHeaders, logger, onInboundReceived, onAuthError }) {
11
+ constructor({ store, hubUrl, getHeaders, logger, onInboundReceived, onAuthError, onOutboundFlushed }) {
12
12
  this.store = store;
13
13
  this.hubUrl = hubUrl;
14
14
  this.logger = logger || console;
15
15
  this.getHeaders = getHeaders;
16
16
  this.onInboundReceived = onInboundReceived || null;
17
17
  this.onAuthError = onAuthError || null;
18
+ this.onOutboundFlushed = onOutboundFlushed || null;
18
19
 
19
20
  this.outbound = new OutboundSync({ store, hubUrl, getHeaders, logger });
20
21
  this.inbound = new InboundSync({ store, hubUrl, getHeaders, logger });
@@ -82,6 +83,11 @@ class SyncEngine {
82
83
  try {
83
84
  const result = await this.outbound.flush();
84
85
  if (result.sent > 0) this._lastActivity = Date.now();
86
+ if ((result.sent > 0 || result.dropped > 0) && typeof this.onOutboundFlushed === 'function') {
87
+ try { this.onOutboundFlushed(result); } catch (e) {
88
+ this.logger.warn?.('[sync] onOutboundFlushed callback failed:', e.message);
89
+ }
90
+ }
85
91
  } catch (err) {
86
92
  if (err instanceof AuthError) {
87
93
  await this._handleAuthError('outbound');
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { PROXY_PROTOCOL_VERSION } = require('../mailbox/store');
4
4
  const { AuthError } = require('../lifecycle/manager');
5
+ const { isProxyTraceUploadPayloadAllowed, resolveTraceMode } = require('../trace/extractor');
5
6
  const { hubFetch } = require('../../gep/hubFetch');
6
7
 
7
8
  const MAX_BATCH = 50;
@@ -16,8 +17,31 @@ class OutboundSync {
16
17
  }
17
18
 
18
19
  async flush(channel = 'evomap-hub') {
19
- const pending = this.store.pollOutbound({ channel, limit: MAX_BATCH });
20
- if (pending.length === 0) return { sent: 0 };
20
+ const pendingBatch = this.store.pollOutbound({ channel, limit: MAX_BATCH });
21
+ if (pendingBatch.length === 0) return { sent: 0 };
22
+
23
+ let pending = pendingBatch;
24
+ const rejectedTraceUploads = [];
25
+ const traceUploadEnabled = resolveTraceMode(process.env, { store: this.store });
26
+ for (const m of pendingBatch) {
27
+ if (m.type !== 'proxy_trace') continue;
28
+ if (!traceUploadEnabled) {
29
+ rejectedTraceUploads.push({ id: m.id, error: 'proxy trace upload disabled' });
30
+ } else if (!isProxyTraceUploadPayloadAllowed(m.payload, process.env)) {
31
+ rejectedTraceUploads.push({ id: m.id, error: 'proxy trace payload rejected' });
32
+ }
33
+ }
34
+ if (rejectedTraceUploads.length > 0) {
35
+ this.store.updateStatusBatch(rejectedTraceUploads.map(m => ({
36
+ id: m.id,
37
+ status: 'rejected',
38
+ error: m.error,
39
+ })));
40
+ const rejectedIds = new Set(rejectedTraceUploads.map(m => m.id));
41
+ pending = pendingBatch.filter(m => !rejectedIds.has(m.id));
42
+ if (pending.length === 0) return { sent: 0, dropped: rejectedTraceUploads.length };
43
+ }
44
+ const dropped = rejectedTraceUploads.length;
21
45
 
22
46
  const endpoint = `${this.hubUrl}/a2a/mailbox/outbound`;
23
47
 
@@ -83,14 +107,18 @@ class OutboundSync {
83
107
  if (inboundMessages.length > 0) this.store.writeInboundBatch(inboundMessages);
84
108
 
85
109
  this.store.setState('last_sync_at', new Date().toISOString());
86
- return { sent: pending.length, synced: updates.length, responses: inboundMessages.length };
110
+ const result = { sent: pending.length, synced: updates.length, responses: inboundMessages.length };
111
+ if (dropped > 0) result.dropped = dropped;
112
+ return result;
87
113
  } catch (err) {
88
114
  if (err instanceof AuthError) throw err;
89
115
  this.logger.error(`[outbound] flush failed: ${err.message}`);
90
116
  for (const m of pending) {
91
117
  this.store.incrementRetry(m.id, err.message);
92
118
  }
93
- return { sent: 0, error: err.message };
119
+ const result = { sent: 0, error: err.message };
120
+ if (dropped > 0) result.dropped = dropped;
121
+ return result;
94
122
  }
95
123
  }
96
124
  }