@evomap/evolver-proxy 2.0.0-beta.9 → 2.0.0

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 (42) hide show
  1. package/dist/bin/evolver-proxy.d.ts +26 -2
  2. package/dist/bin/evolver-proxy.js +284 -51
  3. package/dist/daemon/atpConsent.js +5 -2
  4. package/dist/daemon/collaborationFacade.js +23 -13
  5. package/dist/daemon/proxyDaemon.d.ts +52 -0
  6. package/dist/daemon/proxyDaemon.js +1067 -24
  7. package/dist/daemon/publishRecallVerifier.d.ts +114 -0
  8. package/dist/daemon/publishRecallVerifier.js +495 -0
  9. package/dist/daemon/selectHub.js +5 -3
  10. package/dist/daemon/systemdNotifier.d.ts +46 -0
  11. package/dist/daemon/systemdNotifier.js +153 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +4 -1
  14. package/dist/lifecycle/claimNudge.d.ts +20 -0
  15. package/dist/lifecycle/claimNudge.js +124 -0
  16. package/dist/lifecycle/manager.d.ts +4 -0
  17. package/dist/lifecycle/manager.js +15 -2
  18. package/dist/llm/server.js +24 -4
  19. package/dist/llm/upstream.d.ts +5 -1
  20. package/dist/llm/upstream.js +24 -1
  21. package/dist/private/accountAssetCompatibility.d.ts +29 -0
  22. package/dist/private/accountAssetCompatibility.js +196 -0
  23. package/dist/private/adapterLoader.d.ts +21 -1
  24. package/dist/private/adapterLoader.js +242 -7
  25. package/dist/private/nodeCredentialStore.d.ts +23 -0
  26. package/dist/private/nodeCredentialStore.js +210 -0
  27. package/dist/selfUpdate/bootstrap.d.ts +69 -0
  28. package/dist/selfUpdate/bootstrap.js +282 -0
  29. package/dist/selfUpdate/builtinKey.d.ts +4 -0
  30. package/dist/selfUpdate/builtinKey.js +16 -0
  31. package/dist/selfUpdate/executor.d.ts +1 -1
  32. package/dist/selfUpdate/executor.js +1 -1
  33. package/dist/selfUpdate/failureCodes.d.ts +4 -0
  34. package/dist/selfUpdate/failureCodes.js +7 -0
  35. package/dist/selfUpdate/migration.d.ts +93 -0
  36. package/dist/selfUpdate/migration.js +315 -0
  37. package/dist/selfUpdate/policy.d.ts +19 -2
  38. package/dist/selfUpdate/policy.js +82 -2
  39. package/dist/selfUpdate/releaseBinary.js +4 -1
  40. package/dist/sync/engine.d.ts +12 -0
  41. package/dist/sync/engine.js +255 -64
  42. package/package.json +7 -4
@@ -9,6 +9,9 @@ export const MAX_BATCH = 50;
9
9
  export const IDLE_THRESHOLD_MS = 5 * 60_000;
10
10
  const MAX_SYNC_ERROR_LENGTH = 2_000;
11
11
  const CURSOR_KEY = 'sync:inbound_cursor';
12
+ const ACCEPTED_TASK_OUTBOUND_PREFIX = 'sync:accepted_task_outbound:';
13
+ const ACCEPTED_TASK_JOURNAL_WRITE_ATTEMPTS = 2;
14
+ const LOCAL_FINALIZE_RETRY_MS = 1_000;
12
15
  const BATCHABLE_PROXY_TYPES = new Set(['proxy_trace']);
13
16
  const TRACE_CONFIG_TYPES = new Set(['trace_collection_config', 'proxy_trace_config']);
14
17
  const STATE = {
@@ -95,6 +98,12 @@ function envelopeToAgentEvent(e) {
95
98
  ...(e.type === 'proxy_trace' && e.idempotencyKey ? { refId: e.idempotencyKey } : {}),
96
99
  };
97
100
  }
101
+ function requiredClaimOwner(envelope) {
102
+ const claimOwner = mailbox.mailboxClaimOwner(envelope);
103
+ if (!claimOwner)
104
+ throw new Error(`mailbox_claim_owner_missing:${envelope.id}`);
105
+ return claimOwner;
106
+ }
98
107
  /**
99
108
  * SyncEngine(M6-2): proxy↔hub 双向同步. 移植 v1 sync/{engine,outbound,inbound} 到 TS,
100
109
  * hub I/O 全走 HubCapability.mailbox(非裸 hubFetch). tick 方法纯逻辑(注入 now), 可对 FakeHubCapability 确定性测;
@@ -104,7 +113,13 @@ function applyMailboxPushManyOutcomes(group, outcomes, deps) {
104
113
  const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
105
114
  const result = { sent: 0, failed: 0, terminal: 0, deferred: 0, completedDuplicates: 0 };
106
115
  for (const pushed of group) {
107
- const outcome = byId.get(pushed.event.id) ?? byId.get(pushed.original.id);
116
+ const outcome = byId.get(pushed.event.id) ?? byId.get(pushed.original.id) ?? {
117
+ id: pushed.event.id,
118
+ status: 'failed',
119
+ reason: 'mailbox_response_incomplete',
120
+ retryable: true,
121
+ terminal: false,
122
+ };
108
123
  if (outcome?.status === 'failed') {
109
124
  const msg = redactAndTruncate(outcome.reason ?? 'mailbox_push_rejected');
110
125
  const authLike = isAuthError(msg);
@@ -119,36 +134,47 @@ function applyMailboxPushManyOutcomes(group, outcomes, deps) {
119
134
  result.authFailed = true;
120
135
  result.authErrorMessage ??= msg;
121
136
  }
122
- deps.store.defer(pushed.original.id, msg, nowAfterFailure, retry);
123
- for (const duplicate of pushed.duplicates)
124
- deps.store.defer(duplicate.id, msg, nowAfterFailure, retry);
125
- result.deferred += 1 + pushed.duplicates.length;
137
+ if (deps.store.deferClaimed(pushed.original.id, msg, nowAfterFailure, retry, requiredClaimOwner(pushed.original)))
138
+ result.deferred += 1;
139
+ for (const duplicate of pushed.duplicates) {
140
+ if (deps.store.deferClaimed(duplicate.id, msg, nowAfterFailure, retry, requiredClaimOwner(duplicate))) {
141
+ result.deferred += 1;
142
+ }
143
+ }
126
144
  }
127
145
  else {
128
146
  const maxAttempts = outcome.terminal ? 1 : undefined;
129
- deps.store.fail(pushed.original.id, msg, deps.now(), maxAttempts);
130
- for (const duplicate of pushed.duplicates)
131
- deps.store.fail(duplicate.id, msg, deps.now(), maxAttempts);
147
+ let transitioned = deps.store.failClaimed(pushed.original.id, msg, deps.now(), requiredClaimOwner(pushed.original), maxAttempts) ? 1 : 0;
148
+ for (const duplicate of pushed.duplicates) {
149
+ if (deps.store.failClaimed(duplicate.id, msg, deps.now(), requiredClaimOwner(duplicate), maxAttempts)) {
150
+ transitioned += 1;
151
+ }
152
+ }
132
153
  if (outcome.terminal)
133
- result.terminal += 1 + pushed.duplicates.length;
154
+ result.terminal += transitioned;
134
155
  else
135
- result.failed += 1 + pushed.duplicates.length;
156
+ result.failed += transitioned;
136
157
  }
137
158
  }
138
159
  else {
139
- deps.store.complete(pushed.original.id, deps.now());
140
- for (const duplicate of pushed.duplicates)
141
- deps.store.complete(duplicate.id, deps.now());
160
+ const completedOriginal = deps.store.completeClaimed(pushed.original.id, deps.now(), requiredClaimOwner(pushed.original));
161
+ let completedDuplicates = 0;
162
+ for (const duplicate of pushed.duplicates) {
163
+ if (deps.store.completeClaimed(duplicate.id, deps.now(), requiredClaimOwner(duplicate)))
164
+ completedDuplicates += 1;
165
+ }
142
166
  if (pushed.dedupKey)
143
167
  deps.store.markProcessed(pushed.dedupKey, { type: pushed.original.type }, deps.now());
144
- result.sent += 1;
145
- result.completedDuplicates += pushed.duplicates.length;
168
+ if (completedOriginal)
169
+ result.sent += 1;
170
+ result.completedDuplicates += completedDuplicates;
146
171
  }
147
172
  }
148
173
  return result;
149
174
  }
150
175
  export class SyncEngine {
151
176
  deps;
177
+ acceptedTaskOutboundMemory = new Map();
152
178
  lastActivityAt;
153
179
  constructor(deps) {
154
180
  this.deps = deps;
@@ -160,6 +186,7 @@ export class SyncEngine {
160
186
  const batch = this.deps.store.claim('proxy', limit, this.deps.leaseMs ?? 30_000, now, this.deps.runtimeNamespace);
161
187
  let sent = 0, failed = 0, terminal = 0, deferred = 0, completedDuplicates = 0;
162
188
  let authFailed = false;
189
+ let localPersistenceFailed = false;
163
190
  let authErrorMessage; // first auth-failure detail, carried up so the daemon surfaces the real code (#314)
164
191
  for (let i = 0; i < batch.length; i += 1) {
165
192
  const e = batch[i];
@@ -173,8 +200,9 @@ export class SyncEngine {
173
200
  break;
174
201
  const outboundDedupKey = this.outboundDedupKey(current);
175
202
  if (outboundDedupKey && this.deps.store.isProcessed(outboundDedupKey)) {
176
- this.deps.store.complete(current.id, this.deps.now());
177
- completedDuplicates += 1;
203
+ if (this.deps.store.completeClaimed(current.id, this.deps.now(), requiredClaimOwner(current))) {
204
+ completedDuplicates += 1;
205
+ }
178
206
  continue;
179
207
  }
180
208
  const existing = outboundDedupKey ? groupByDedupKey.get(outboundDedupKey) : undefined;
@@ -184,8 +212,9 @@ export class SyncEngine {
184
212
  }
185
213
  const guard = this.normalizeOutbound(current);
186
214
  if (!guard.ok) {
187
- this.deps.store.fail(current.id, guard.reason, this.deps.now(), 1);
188
- terminal += 1;
215
+ if (this.deps.store.failClaimed(current.id, guard.reason, this.deps.now(), requiredClaimOwner(current), 1)) {
216
+ terminal += 1;
217
+ }
189
218
  continue;
190
219
  }
191
220
  const entry = {
@@ -220,22 +249,24 @@ export class SyncEngine {
220
249
  if (applied.deferredFailure) {
221
250
  const nowAfterFailure = this.deps.now();
222
251
  for (const pending of batch.slice(j)) {
223
- this.deps.store.defer(pending.id, applied.deferredFailure.msg, nowAfterFailure, applied.deferredFailure.retryAfterMs);
224
- deferred += 1;
252
+ if (this.deps.store.deferClaimed(pending.id, applied.deferredFailure.msg, nowAfterFailure, applied.deferredFailure.retryAfterMs, requiredClaimOwner(pending)))
253
+ deferred += 1;
225
254
  }
226
255
  break;
227
256
  }
228
257
  }
229
258
  else {
230
259
  for (const pushed of group) {
231
- this.deps.store.complete(pushed.original.id, this.deps.now());
232
- for (const duplicate of pushed.duplicates)
233
- this.deps.store.complete(duplicate.id, this.deps.now());
260
+ if (this.deps.store.completeClaimed(pushed.original.id, this.deps.now(), requiredClaimOwner(pushed.original)))
261
+ sent += 1;
262
+ for (const duplicate of pushed.duplicates) {
263
+ if (this.deps.store.completeClaimed(duplicate.id, this.deps.now(), requiredClaimOwner(duplicate))) {
264
+ completedDuplicates += 1;
265
+ }
266
+ }
234
267
  if (pushed.dedupKey)
235
268
  this.deps.store.markProcessed(pushed.dedupKey, { type: pushed.original.type }, this.deps.now());
236
- completedDuplicates += pushed.duplicates.length;
237
269
  }
238
- sent += group.length;
239
270
  }
240
271
  i = j - 1;
241
272
  continue;
@@ -248,14 +279,17 @@ export class SyncEngine {
248
279
  const nowAfterFailure = this.deps.now();
249
280
  const retry = retryAfterMs(err);
250
281
  for (const pushed of group) {
251
- this.deps.store.defer(pushed.original.id, msg, nowAfterFailure, retry);
252
- for (const duplicate of pushed.duplicates)
253
- this.deps.store.defer(duplicate.id, msg, nowAfterFailure, retry);
254
- deferred += 1 + pushed.duplicates.length;
282
+ if (this.deps.store.deferClaimed(pushed.original.id, msg, nowAfterFailure, retry, requiredClaimOwner(pushed.original)))
283
+ deferred += 1;
284
+ for (const duplicate of pushed.duplicates) {
285
+ if (this.deps.store.deferClaimed(duplicate.id, msg, nowAfterFailure, retry, requiredClaimOwner(duplicate)))
286
+ deferred += 1;
287
+ }
255
288
  }
256
289
  for (const pending of batch.slice(j)) {
257
- this.deps.store.defer(pending.id, msg, nowAfterFailure, retry);
258
- deferred += 1;
290
+ if (this.deps.store.deferClaimed(pending.id, msg, nowAfterFailure, retry, requiredClaimOwner(pending))) {
291
+ deferred += 1;
292
+ }
259
293
  }
260
294
  authFailed = true;
261
295
  authErrorMessage ??= msg;
@@ -263,10 +297,12 @@ export class SyncEngine {
263
297
  }
264
298
  else if (isTerminal(err, group[0].original.type)) {
265
299
  for (const pushed of group) {
266
- this.deps.store.fail(pushed.original.id, msg, this.deps.now(), 1);
267
- for (const duplicate of pushed.duplicates)
268
- this.deps.store.fail(duplicate.id, msg, this.deps.now(), 1);
269
- terminal += 1 + pushed.duplicates.length;
300
+ if (this.deps.store.failClaimed(pushed.original.id, msg, this.deps.now(), requiredClaimOwner(pushed.original), 1))
301
+ terminal += 1;
302
+ for (const duplicate of pushed.duplicates) {
303
+ if (this.deps.store.failClaimed(duplicate.id, msg, this.deps.now(), requiredClaimOwner(duplicate), 1))
304
+ terminal += 1;
305
+ }
270
306
  }
271
307
  i = j - 1;
272
308
  }
@@ -274,23 +310,27 @@ export class SyncEngine {
274
310
  const nowAfterFailure = this.deps.now();
275
311
  const retry = retryAfterMs(err);
276
312
  for (const pushed of group) {
277
- this.deps.store.defer(pushed.original.id, msg, nowAfterFailure, retry);
278
- for (const duplicate of pushed.duplicates)
279
- this.deps.store.defer(duplicate.id, msg, nowAfterFailure, retry);
280
- deferred += 1 + pushed.duplicates.length;
313
+ if (this.deps.store.deferClaimed(pushed.original.id, msg, nowAfterFailure, retry, requiredClaimOwner(pushed.original)))
314
+ deferred += 1;
315
+ for (const duplicate of pushed.duplicates) {
316
+ if (this.deps.store.deferClaimed(duplicate.id, msg, nowAfterFailure, retry, requiredClaimOwner(duplicate)))
317
+ deferred += 1;
318
+ }
281
319
  }
282
320
  for (const pending of batch.slice(j)) {
283
- this.deps.store.defer(pending.id, msg, nowAfterFailure, retry);
284
- deferred += 1;
321
+ if (this.deps.store.deferClaimed(pending.id, msg, nowAfterFailure, retry, requiredClaimOwner(pending)))
322
+ deferred += 1;
285
323
  }
286
324
  break;
287
325
  }
288
326
  else {
289
327
  for (const pushed of group) {
290
- this.deps.store.fail(pushed.original.id, msg, this.deps.now());
291
- for (const duplicate of pushed.duplicates)
292
- this.deps.store.fail(duplicate.id, msg, this.deps.now());
293
- failed += 1 + pushed.duplicates.length;
328
+ if (this.deps.store.failClaimed(pushed.original.id, msg, this.deps.now(), requiredClaimOwner(pushed.original)))
329
+ failed += 1;
330
+ for (const duplicate of pushed.duplicates) {
331
+ if (this.deps.store.failClaimed(duplicate.id, msg, this.deps.now(), requiredClaimOwner(duplicate)))
332
+ failed += 1;
333
+ }
294
334
  }
295
335
  i = j - 1;
296
336
  }
@@ -299,25 +339,99 @@ export class SyncEngine {
299
339
  }
300
340
  const outboundDedupKey = this.outboundDedupKey(e);
301
341
  if (outboundDedupKey && this.deps.store.isProcessed(outboundDedupKey)) {
302
- this.deps.store.complete(e.id, this.deps.now());
303
- completedDuplicates += 1;
342
+ if (this.deps.store.completeClaimed(e.id, this.deps.now(), requiredClaimOwner(e)))
343
+ completedDuplicates += 1;
304
344
  continue;
305
345
  }
306
346
  const guard = this.normalizeOutbound(e);
307
347
  if (!guard.ok) {
308
- this.deps.store.fail(e.id, redactAndTruncate(guard.reason), this.deps.now(), 1);
309
- terminal += 1;
348
+ if (this.deps.store.failClaimed(e.id, redactAndTruncate(guard.reason), this.deps.now(), requiredClaimOwner(e), 1))
349
+ terminal += 1;
310
350
  continue;
311
351
  }
352
+ const claimOwner = requiredClaimOwner(e);
353
+ let handlerResult;
354
+ let hubAccepted = false;
312
355
  try {
313
- const handlerResult = await this.deps.proxyHandler(guard.envelope);
314
- await this.deps.onOutboundSucceeded?.(e, handlerResult);
315
- this.deps.store.complete(e.id, this.deps.now());
356
+ const accepted = this.acceptedTaskOutbound(e);
357
+ handlerResult = accepted ? accepted.result : await this.deps.proxyHandler(guard.envelope);
358
+ hubAccepted = true;
359
+ let journalError;
360
+ try {
361
+ this.rememberAcceptedTaskOutbound(e, handlerResult);
362
+ }
363
+ catch (err) {
364
+ journalError = `accepted_journal: ${safeErrorMessage(err)}`;
365
+ }
366
+ if (!this.deps.store.renewClaim(e.id, this.deps.now(), this.deps.leaseMs ?? 30_000, claimOwner))
367
+ continue;
368
+ try {
369
+ await this.deps.onOutboundSucceeded?.(e, handlerResult);
370
+ }
371
+ catch (err) {
372
+ const msg = `local_finalize: ${safeErrorMessage(err)}`;
373
+ localPersistenceFailed = true;
374
+ if (journalError && !this.durableAcceptedTaskOutbound(e)) {
375
+ try {
376
+ this.rememberAcceptedTaskOutbound(e, handlerResult);
377
+ journalError = undefined;
378
+ }
379
+ catch (retryError) {
380
+ journalError = `accepted_journal_retry: ${safeErrorMessage(retryError)}`;
381
+ this.markError(journalError, false);
382
+ }
383
+ }
384
+ this.markError(msg, false);
385
+ if (this.durableAcceptedTaskOutbound(e)) {
386
+ if (this.deps.store.deferClaimed(e.id, msg, this.deps.now(), LOCAL_FINALIZE_RETRY_MS, claimOwner)) {
387
+ deferred += 1;
388
+ }
389
+ continue;
390
+ }
391
+ // The Hub has already accepted this envelope. A local observer must not cause a second external action.
392
+ }
393
+ if (!this.deps.store.completeClaimed(e.id, this.deps.now(), claimOwner))
394
+ continue;
395
+ this.acceptedTaskOutboundMemory.delete(e.id);
316
396
  if (outboundDedupKey)
317
397
  this.deps.store.markProcessed(outboundDedupKey, { type: e.type }, this.deps.now());
398
+ if (journalError)
399
+ this.markError(journalError, false);
400
+ if (journalError)
401
+ localPersistenceFailed = true;
318
402
  sent += 1;
319
403
  }
320
404
  catch (err) {
405
+ if (hubAccepted) {
406
+ if (this.deps.store.getById(e.id)?.status === 'done') {
407
+ this.acceptedTaskOutboundMemory.delete(e.id);
408
+ sent += 1;
409
+ continue;
410
+ }
411
+ const msg = `local_finalize: ${safeErrorMessage(err)}`;
412
+ localPersistenceFailed = true;
413
+ if (!this.durableAcceptedTaskOutbound(e)) {
414
+ try {
415
+ this.rememberAcceptedTaskOutbound(e, handlerResult);
416
+ }
417
+ catch (journalRetryError) {
418
+ this.markError(`accepted_journal_retry: ${safeErrorMessage(journalRetryError)}`, false);
419
+ }
420
+ }
421
+ this.markError(msg, false);
422
+ if (this.durableAcceptedTaskOutbound(e)) {
423
+ if (this.deps.store.deferClaimed(e.id, msg, this.deps.now(), LOCAL_FINALIZE_RETRY_MS, claimOwner)) {
424
+ deferred += 1;
425
+ }
426
+ }
427
+ else {
428
+ // No durable local replay is possible for this callback, but the external action is already complete.
429
+ if (this.deps.store.completeClaimed(e.id, this.deps.now(), claimOwner))
430
+ sent += 1;
431
+ this.acceptedTaskOutboundMemory.delete(e.id);
432
+ }
433
+ continue;
434
+ }
321
435
  const msg = safeErrorMessage(err);
322
436
  const authLike = isAuthError(err);
323
437
  this.markError(`outbound: ${msg}`, authLike);
@@ -325,18 +439,34 @@ export class SyncEngine {
325
439
  const nowAfterFailure = this.deps.now();
326
440
  const retry = retryAfterMs(err);
327
441
  for (const pending of batch.slice(i)) {
328
- this.deps.store.defer(pending.id, msg, nowAfterFailure, retry); // Auth recovery owns retries; mailbox attempts stay intact.
329
- deferred += 1;
442
+ const acceptedKey = this.deps.acceptedOutcomeKey?.(pending);
443
+ const pendingClaimOwner = requiredClaimOwner(pending);
444
+ const transitioned = acceptedKey
445
+ ? this.deps.store.deferClaimedUnlessProcessed(pending.id, acceptedKey, msg, nowAfterFailure, retry, pendingClaimOwner)
446
+ : this.deps.store.deferClaimed(pending.id, msg, nowAfterFailure, retry, pendingClaimOwner);
447
+ if (transitioned)
448
+ deferred += 1;
449
+ else if (this.completeAcceptedOutcome(pending))
450
+ sent += 1;
330
451
  }
331
452
  authFailed = true;
332
453
  authErrorMessage ??= msg;
333
454
  break;
334
455
  }
335
456
  else if (isTerminal(err, e.type)) {
336
- // A concurrent facade attempt may already have finalized this durable intent successfully.
337
- if (this.deps.store.getById(e.id)?.status === 'done')
457
+ const transitionNow = this.deps.now();
458
+ const acceptedKey = this.deps.acceptedOutcomeKey?.(e);
459
+ const terminalOutcome = this.deps.terminalOutcome?.(e, err);
460
+ const transitioned = acceptedKey && terminalOutcome
461
+ ? this.deps.store.failClaimedAndMarkProcessedUnlessProcessed(e.id, [acceptedKey], terminalOutcome.key, terminalOutcome.result, msg, transitionNow, claimOwner, 1)
462
+ : acceptedKey
463
+ ? this.deps.store.failClaimedUnlessProcessed(e.id, acceptedKey, msg, transitionNow, claimOwner, 1)
464
+ : this.deps.store.failClaimed(e.id, msg, transitionNow, claimOwner, 1);
465
+ if (!transitioned) {
466
+ if (this.completeAcceptedOutcome(e))
467
+ sent += 1;
338
468
  continue;
339
- this.deps.store.fail(e.id, msg, this.deps.now(), 1); // maxAttempts=1 → 直进 DLQ, 不反复打经济端点
469
+ }
340
470
  await this.deps.onOutboundTerminal?.(e, err);
341
471
  terminal += 1;
342
472
  }
@@ -344,20 +474,34 @@ export class SyncEngine {
344
474
  const nowAfterFailure = this.deps.now();
345
475
  const retry = retryAfterMs(err);
346
476
  for (const pending of batch.slice(i)) {
347
- this.deps.store.defer(pending.id, msg, nowAfterFailure, retry); // Hub outage 不消耗 attempts
348
- deferred += 1;
477
+ const acceptedKey = this.deps.acceptedOutcomeKey?.(pending);
478
+ const pendingClaimOwner = requiredClaimOwner(pending);
479
+ const transitioned = acceptedKey
480
+ ? this.deps.store.deferClaimedUnlessProcessed(pending.id, acceptedKey, msg, nowAfterFailure, retry, pendingClaimOwner)
481
+ : this.deps.store.deferClaimed(pending.id, msg, nowAfterFailure, retry, pendingClaimOwner);
482
+ if (transitioned)
483
+ deferred += 1;
484
+ else if (this.completeAcceptedOutcome(pending))
485
+ sent += 1;
349
486
  }
350
487
  break;
351
488
  }
352
489
  else {
353
- this.deps.store.fail(e.id, msg, this.deps.now()); // 退避重试
354
- failed += 1;
490
+ const transitionNow = this.deps.now();
491
+ const acceptedKey = this.deps.acceptedOutcomeKey?.(e);
492
+ const transitioned = acceptedKey
493
+ ? this.deps.store.failClaimedUnlessProcessed(e.id, acceptedKey, msg, transitionNow, claimOwner)
494
+ : this.deps.store.failClaimed(e.id, msg, transitionNow, claimOwner);
495
+ if (transitioned)
496
+ failed += 1;
497
+ else if (this.completeAcceptedOutcome(e))
498
+ sent += 1;
355
499
  }
356
500
  }
357
501
  }
358
502
  if (sent > 0)
359
503
  this.lastActivityAt = now;
360
- if (failed === 0 && terminal === 0 && deferred === 0)
504
+ if (failed === 0 && terminal === 0 && deferred === 0 && !localPersistenceFailed)
361
505
  this.markOk();
362
506
  if (sent > 0 || terminal > 0 || completedDuplicates > 0) {
363
507
  await this.notifyOutboundFlushed({ sent, failed, terminal, deferred });
@@ -409,7 +553,7 @@ export class SyncEngine {
409
553
  };
410
554
  }
411
555
  catch (err) {
412
- if (isHubUnreachable(err)) {
556
+ if (isHubUnreachable(err) || (err instanceof HubClientError && err.status === 429)) {
413
557
  const msg = `inbound: ${safeErrorMessage(err)}`;
414
558
  this.markError(msg, false);
415
559
  return { received: 0, enqueued: 0, hasMore: false, nextPollAfterMs: retryAfterMs(err) };
@@ -482,6 +626,53 @@ export class SyncEngine {
482
626
  return null;
483
627
  return e.idempotencyKey ? `outbound:${e.idempotencyKey}` : null;
484
628
  }
629
+ acceptedTaskOutbound(e) {
630
+ if (e.type !== 'task_claim' && e.type !== 'task_complete')
631
+ return undefined;
632
+ return this.durableAcceptedTaskOutbound(e) ?? this.acceptedTaskOutboundMemory.get(e.id);
633
+ }
634
+ durableAcceptedTaskOutbound(e) {
635
+ if (e.type !== 'task_claim' && e.type !== 'task_complete')
636
+ return undefined;
637
+ const value = this.deps.store.getProcessed(`${ACCEPTED_TASK_OUTBOUND_PREFIX}${e.id}`);
638
+ if (value && typeof value === 'object' && value.accepted === true) {
639
+ return value;
640
+ }
641
+ const checkpoint = this.deps.store.getClaimedCheckpoint(e.id);
642
+ if (checkpoint && typeof checkpoint === 'object'
643
+ && checkpoint.accepted === true) {
644
+ return checkpoint;
645
+ }
646
+ return undefined;
647
+ }
648
+ completeAcceptedOutcome(e) {
649
+ const acceptedKey = this.deps.acceptedOutcomeKey?.(e);
650
+ if (!acceptedKey || !this.deps.store.isProcessed(acceptedKey))
651
+ return false;
652
+ return this.deps.store.completeClaimed(e.id, this.deps.now(), requiredClaimOwner(e));
653
+ }
654
+ rememberAcceptedTaskOutbound(e, result) {
655
+ if (e.type !== 'task_claim' && e.type !== 'task_complete')
656
+ return;
657
+ const accepted = { accepted: true, result };
658
+ this.acceptedTaskOutboundMemory.set(e.id, accepted);
659
+ let lastError;
660
+ for (let attempt = 0; attempt < ACCEPTED_TASK_JOURNAL_WRITE_ATTEMPTS; attempt += 1) {
661
+ try {
662
+ this.deps.store.markProcessed(`${ACCEPTED_TASK_OUTBOUND_PREFIX}${e.id}`, accepted, this.deps.now());
663
+ this.acceptedTaskOutboundMemory.delete(e.id);
664
+ return;
665
+ }
666
+ catch (error) {
667
+ lastError = error;
668
+ }
669
+ }
670
+ if (this.deps.store.setClaimedCheckpoint(e.id, accepted, this.deps.now(), requiredClaimOwner(e))) {
671
+ this.acceptedTaskOutboundMemory.delete(e.id);
672
+ return;
673
+ }
674
+ throw lastError;
675
+ }
485
676
  async safeAck(id) {
486
677
  try {
487
678
  await this.deps.hub.mailbox.ack(id);
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@evomap/evolver-proxy",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": "^22.13.0 || >=23.4.0"
8
+ },
6
9
  "description": "系统级 mailbox/hub 同步 daemon (Node)",
7
10
  "main": "./dist/index.js",
8
11
  "types": "./dist/index.d.ts",
@@ -26,8 +29,8 @@
26
29
  },
27
30
  "dependencies": {
28
31
  "@aws-sdk/client-bedrock-runtime": "^3.1053.0",
29
- "@evomap/evolver-adapter-public": "2.0.0-beta.9",
30
- "@evomap/evolver-core": "2.0.0-beta.9"
32
+ "@evomap/evolver-adapter-public": "2.0.0",
33
+ "@evomap/evolver-core": "2.0.0"
31
34
  },
32
35
  "repository": {
33
36
  "type": "git",
@@ -35,7 +38,7 @@
35
38
  },
36
39
  "publishConfig": {
37
40
  "access": "public",
38
- "tag": "v2-beta"
41
+ "tag": "latest"
39
42
  },
40
43
  "files": [
41
44
  "dist/",