@evomap/evolver-proxy 2.0.0-beta.8 → 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
@@ -1,5 +1,7 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
1
2
  import { dirname, join } from 'node:path';
2
- import { mailbox, hub as hubNs, shadow as shadow_, assetstore, util } from '@evomap/evolver-core';
3
+ import { mailbox, hub as hubNs, shadow as shadow_, assetstore, wire, util } from '@evomap/evolver-core';
4
+ import { AuthError, HubClientError, HubUnreachableError } from '@evomap/evolver-adapter-public';
3
5
  import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
4
6
  import { LifecycleManager } from '../lifecycle/manager.js';
5
7
  import { executeForceUpdate } from '../selfUpdate/executor.js';
@@ -7,11 +9,40 @@ import { reportPendingSelfUpdateLastUpdate, reportSelfUpdateLastUpdate } from '.
7
9
  import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
8
10
  import { hubAuthFailureHint } from './selectHub.js';
9
11
  import { CollaborationFacade } from './collaborationFacade.js';
12
+ import { PublishRecallVerifier, resolvePublishRecallConfig, } from './publishRecallVerifier.js';
10
13
  export const DEFAULT_IPC_PORT = 19820;
14
+ // V1 local-proxy compatibility contract; independent of the V2 mailbox envelope schema.
15
+ const PROXY_PROTOCOL_VERSION = '0.1.0';
16
+ const PROXY_STATUS_SCHEMA_VERSION = 1;
11
17
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
12
18
  const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
13
19
  const MAX_HEARTBEAT_TICK_ERROR_LENGTH = 1_000;
14
20
  const MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS = 5;
21
+ const DEFAULT_ASSET_SEARCH_CACHE_TTL_MS = 30_000;
22
+ const DEFAULT_ASSET_SEARCH_CACHE_MAX = 256;
23
+ const DEFAULT_ASSET_SEARCH_STALE_GRACE_MS = 5 * 60_000;
24
+ const MAX_ASSET_SUBMIT_ITEMS = 50;
25
+ const ASYNC_ASSET_SUBMIT_REQUEST_ID = /^[A-Za-z0-9._:-]{1,128}$/;
26
+ const ASYNC_ASSET_SUBMIT_PREFIX = 'async_asset_submit:';
27
+ const OUTBOUND_HUB_MODE_FIELD = '__evolver_hub_mode';
28
+ const SYNC_ASSET_SUBMIT_PREFIX = 'sync_asset_submit:';
29
+ const SYNC_ASSET_SUBMIT_TYPE_RANK = {
30
+ Gene: 0,
31
+ Capsule: 1,
32
+ EvolutionEvent: 2,
33
+ AntiGene: 3,
34
+ };
35
+ const SYNC_ASSET_SUBMIT_SCOPE_STATE_KEY = 'sync_asset_submit:idempotency_scope:v1';
36
+ const SYNC_ASSET_SUBMIT_DIRECT_RETRY_GRACE_MS = 30_000;
37
+ const DEFAULT_SYNC_ASSET_SUBMIT_RESPONSE_TIMEOUT_MS = 15_000;
38
+ class OutboundHubModeMismatchError extends Error {
39
+ retryable = true;
40
+ retryAfterMs = 1_000;
41
+ constructor(expected, actual) {
42
+ super(`asset_submit hub mode mismatch: queued for ${expected}, running in ${actual}`);
43
+ this.name = 'OutboundHubModeMismatchError';
44
+ }
45
+ }
15
46
  /**
16
47
  * ProxyDaemon(M6-4) 装配层: 把 core(MailboxStore/Dispatcher/MailboxDaemon/IpcServer) +
17
48
  * HubBindings(M6-1) + SyncEngine(M6-2) + LifecycleManager(M6-3) 拼成系统级 proxy.
@@ -31,9 +62,21 @@ export class ProxyDaemon {
31
62
  validator;
32
63
  atp;
33
64
  collaborationFacade;
65
+ publishRecallVerifier;
66
+ proxyHandler;
34
67
  ipc;
35
68
  now;
36
69
  random;
70
+ assetSearchCacheTtlMs;
71
+ assetSearchCacheMax;
72
+ assetSearchStaleGraceMs;
73
+ assetSubmitResponseTimeoutMs;
74
+ synchronousAssetSubmitScope;
75
+ shadowMode;
76
+ assetSearchCache = new Map();
77
+ assetSearchInflight = new Map();
78
+ synchronousAssetSubmitInflight = new Map();
79
+ assetSearchCooldownUntil = 0;
37
80
  nextHeartbeatAt;
38
81
  heartbeatFailures = 0;
39
82
  heartbeatGeneration = 0;
@@ -42,6 +85,10 @@ export class ProxyDaemon {
42
85
  /** A poke that arrived between ticks (no sleep in flight) parks the wake here so it is not lost. */
43
86
  wakeRunnerPending = false;
44
87
  started = false;
88
+ lifecycleArmed = false;
89
+ lastTickAt;
90
+ nextTickDueAt;
91
+ consecutiveTickFailures = 0;
45
92
  storeClosed = false;
46
93
  forceUpdateTriggerInFlight = false;
47
94
  forceUpdateLastTriggeredAt;
@@ -55,20 +102,34 @@ export class ProxyDaemon {
55
102
  this.deps = deps;
56
103
  this.now = deps.now ?? (() => Date.now());
57
104
  this.random = deps.random ?? Math.random;
105
+ this.assetSearchCacheTtlMs = positiveIntegerOr(deps.assetSearchCacheTtlMs, DEFAULT_ASSET_SEARCH_CACHE_TTL_MS);
106
+ this.assetSearchCacheMax = positiveIntegerOr(deps.assetSearchCacheMax, DEFAULT_ASSET_SEARCH_CACHE_MAX);
107
+ this.assetSearchStaleGraceMs = positiveIntegerOr(deps.assetSearchStaleGraceMs, DEFAULT_ASSET_SEARCH_STALE_GRACE_MS);
108
+ this.assetSubmitResponseTimeoutMs = positiveIntegerOr(deps.assetSubmitResponseTimeoutMs, DEFAULT_SYNC_ASSET_SUBMIT_RESPONSE_TIMEOUT_MS);
58
109
  if (!deps.store && !deps.storePath)
59
110
  throw new Error('ProxyDaemon: 需 store 或 storePath 之一');
60
111
  const shadow = deps.shadowMode === 'shadow';
112
+ this.shadowMode = shadow;
61
113
  if (shadow && !deps.shadowSink)
62
114
  throw new Error('ProxyDaemon: shadow 模式需 shadowSink');
63
115
  // M8 shadow 装配: 在边界包 decorator, 下游 makeHubBindings/Dispatcher/SyncEngine/MailboxDaemon 零改.
64
116
  this.store = deps.store
65
117
  ?? (shadow ? new shadow_.ShadowMailboxStore({ path: deps.storePath }, deps.shadowSink, 'shadow') : new mailbox.MailboxStore({ path: deps.storePath }));
118
+ const existingSynchronousAssetSubmitScope = this.store.getState(SYNC_ASSET_SUBMIT_SCOPE_STATE_KEY);
119
+ this.synchronousAssetSubmitScope = existingSynchronousAssetSubmitScope ?? randomUUID();
120
+ if (!existingSynchronousAssetSubmitScope) {
121
+ this.store.setState(SYNC_ASSET_SUBMIT_SCOPE_STATE_KEY, this.synchronousAssetSubmitScope);
122
+ }
66
123
  const assetStoreDir = deps.assetStoreDir ?? (deps.storePath ? join(dirname(deps.storePath), 'assets') : undefined);
67
124
  this.assetStore = deps.assetStore ?? (assetStoreDir ? new assetstore.LocalJsonlProvider(assetStoreDir) : undefined);
68
125
  this.atp = deps.atp;
69
126
  const hubToUse = shadow ? shadow_.shadowHubCapability(deps.hub, deps.shadowSink, 'shadow') : deps.hub;
70
- const hubBindings = hubNs.makeHubBindings(hubToUse);
71
- const proxyHandler = hubBindings.asProxyHandler();
127
+ const hubBindings = hubNs.makeHubBindings(hubToUse, deps.publishSanitizeEnv
128
+ ? { sanitize: { env: deps.publishSanitizeEnv } }
129
+ : {});
130
+ this.proxyHandler = hubBindings.asProxyHandler();
131
+ const proxyHandler = this.proxyHandler;
132
+ const syncProxyHandler = (envelope) => this.handleHubModeBoundOutbound(envelope);
72
133
  const assetByIdSource = isAssetByIdFetcher(deps.hub) ? deps.hub : (isAssetByIdFetcher(hubToUse) ? hubToUse : undefined);
73
134
  this.remoteAssetById = assetByIdSource
74
135
  ? async (assetId) => {
@@ -76,6 +137,17 @@ export class ProxyDaemon {
76
137
  return assetMatchesId(fetched, assetId) ? fetched : null;
77
138
  }
78
139
  : undefined;
140
+ const publishRecallConfig = resolvePublishRecallConfig();
141
+ this.publishRecallVerifier = deps.publishRecallVerifier ?? new PublishRecallVerifier({
142
+ store: this.store,
143
+ ...(!shadow && assetByIdSource
144
+ ? { fetchAssetById: (assetId) => assetByIdSource.fetchAssetById(assetId) }
145
+ : {}),
146
+ config: shadow ? { ...publishRecallConfig, enabled: false } : publishRecallConfig,
147
+ now: this.now,
148
+ random: this.random,
149
+ stateKey: `publish_recall_verifier:${deps.runtimeNamespace ?? 'default'}:v1`,
150
+ });
79
151
  this.reuseResultReporter = isReuseResultReporter(hubToUse)
80
152
  ? hubToUse
81
153
  : (!shadow && isReuseResultReporter(deps.hub) ? deps.hub : undefined);
@@ -105,15 +177,50 @@ export class ProxyDaemon {
105
177
  ...(deps.collaborationOperationTimeoutMs !== undefined ? { operationTimeoutMs: deps.collaborationOperationTimeoutMs } : {}),
106
178
  });
107
179
  this.sync = new SyncEngine({
108
- store: this.store, hub: hubToUse, proxyHandler, now: this.now,
180
+ store: this.store, hub: hubToUse, proxyHandler: syncProxyHandler, now: this.now,
109
181
  ...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
110
- onOutboundSucceeded: (envelope, result) => this.collaborationFacade.handleOutboundSucceeded(envelope, result),
111
- onOutboundTerminal: (envelope, error) => this.collaborationFacade.handleOutboundTerminal(envelope, error),
182
+ onOutboundSucceeded: (envelope, result) => {
183
+ this.collaborationFacade.handleOutboundSucceeded(envelope, result);
184
+ let shouldObserve = true;
185
+ try {
186
+ const cached = this.cacheSynchronousAssetSubmitSuccess(envelope, result);
187
+ if (cached === false)
188
+ shouldObserve = false;
189
+ }
190
+ catch { /* best-effort */ }
191
+ // Observability must never turn a Hub-accepted publish into a failed/retried economic action.
192
+ if (shouldObserve) {
193
+ try {
194
+ this.publishRecallVerifier.observeAcceptedPublish(envelope, result);
195
+ }
196
+ catch { /* best-effort */ }
197
+ }
198
+ },
199
+ onOutboundTerminal: (envelope, error) => {
200
+ this.collaborationFacade.handleOutboundTerminal(envelope, error);
201
+ try {
202
+ this.cacheSynchronousAssetSubmitTerminal(envelope, error);
203
+ }
204
+ catch { /* best-effort */ }
205
+ },
206
+ acceptedOutcomeKey: (envelope) => !shadow && isSynchronousAssetSubmitEnvelope(envelope)
207
+ ? synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey)
208
+ : undefined,
209
+ terminalOutcome: (envelope, error) => {
210
+ if (shadow || !isSynchronousAssetSubmitEnvelope(envelope))
211
+ return undefined;
212
+ const failure = mapSynchronousPublishFailure(error);
213
+ return {
214
+ key: synchronousAssetSubmitOutcomeKey(envelope.idempotencyKey),
215
+ result: { kind: 'failed', ...failure },
216
+ };
217
+ },
112
218
  normalizeInboundEnvelope: (envelope) => this.collaborationFacade.normalizeInboundEnvelope(envelope),
113
219
  ...(deps.traceBackfill ? { onOutboundFlushed: () => { this.drainProxyTraceBackfill(); } } : {}),
114
220
  });
115
221
  this.lifecycle = new LifecycleManager({
116
222
  store: this.store, auth: hubToUse.auth, hello: deps.hello, heartbeat: deps.heartbeat, now: this.now,
223
+ ...(deps.heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs: deps.heartbeatIntervalMs } : {}),
117
224
  ...(deps.evolverVersion ? { evolverVersion: deps.evolverVersion } : {}),
118
225
  ...(deps.helloMode ? { helloMode: deps.helloMode } : {}),
119
226
  onForceUpdateDirective: (directive, source) => { this.triggerForceUpdateFromHeartbeat(directive, source); },
@@ -157,6 +264,7 @@ export class ProxyDaemon {
157
264
  this.daemon.start();
158
265
  this.ipc = new mailbox.MailboxIpcServer({
159
266
  store: this.store, token: this.deps.ipcToken,
267
+ runtimeNamespace: this.deps.runtimeNamespace ?? 'default',
160
268
  ...(this.deps.ipcHost ? { host: this.deps.ipcHost } : {}), now: this.now,
161
269
  onSend: (env, result) => {
162
270
  if (result.stored && env.handler === 'proxy')
@@ -171,7 +279,12 @@ export class ProxyDaemon {
171
279
  }
172
280
  catch { /* local discovery publishing must not block daemon startup */ }
173
281
  await this.lifecycle.doHello();
282
+ this.lifecycleArmed = true;
174
283
  this.drainProxyTraceBackfill();
284
+ try {
285
+ this.publishRecallVerifier.start();
286
+ }
287
+ catch { /* verifier availability must not block proxy startup */ }
175
288
  this.started = true;
176
289
  return port;
177
290
  }
@@ -186,6 +299,7 @@ export class ProxyDaemon {
186
299
  }
187
300
  catch { /* best-effort cleanup */ }
188
301
  this.started = false;
302
+ this.lifecycleArmed = false;
189
303
  throw err;
190
304
  }
191
305
  }
@@ -296,11 +410,14 @@ export class ProxyDaemon {
296
410
  catch { /* ignore telemetry persistence failures */ }
297
411
  }
298
412
  const failedPhases = uniqueTickPhases(errors.map((err) => err.phase));
413
+ const fatalCandidate = errors.length > 0 && isFatalTickCandidate(outbound, inbound, failedPhases);
414
+ this.lastTickAt = this.now();
415
+ this.consecutiveTickFailures = fatalCandidate ? this.consecutiveTickFailures + 1 : 0;
299
416
  return {
300
417
  outbound,
301
418
  inbound,
302
419
  ...(heartbeat ? { heartbeat } : {}),
303
- ...(errors.length > 0 ? { errors, failedPhases, fatalCandidate: isFatalTickCandidate(outbound, inbound, failedPhases) } : { failedPhases: [], fatalCandidate: false }),
420
+ ...(errors.length > 0 ? { errors, failedPhases, fatalCandidate } : { failedPhases: [], fatalCandidate: false }),
304
421
  };
305
422
  }
306
423
  /** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
@@ -317,6 +434,11 @@ export class ProxyDaemon {
317
434
  setWakeHandler(wake) {
318
435
  this.loopWakeHandler = wake;
319
436
  }
437
+ setExpectedNextTick(delayMs) {
438
+ this.nextTickDueAt = delayMs === undefined
439
+ ? undefined
440
+ : this.now() + Math.max(0, delayMs);
441
+ }
320
442
  notifyNewOutbound() {
321
443
  if (this.loopWakeHandler) {
322
444
  this.loopWakeHandler();
@@ -374,11 +496,18 @@ export class ProxyDaemon {
374
496
  return {
375
497
  running: this.started,
376
498
  ipcListening: !!this.ipc,
499
+ lifecycleArmed: this.lifecycleArmed,
377
500
  ...(this.lifecycle.nodeId ? { nodeId: this.lifecycle.nodeId } : {}),
378
501
  lastWriteAt: this.daemon.lastWriteAt(),
502
+ ...(this.lastTickAt !== undefined ? { lastTickAt: this.lastTickAt } : {}),
503
+ ...(this.nextTickDueAt !== undefined ? { nextTickDueAt: this.nextTickDueAt } : {}),
504
+ consecutiveFailures: this.consecutiveTickFailures,
379
505
  };
380
506
  }
381
507
  async stop() {
508
+ this.started = false;
509
+ this.lifecycleArmed = false;
510
+ this.nextTickDueAt = undefined;
382
511
  if (this.forceUpdateTimer) {
383
512
  clearTimeout(this.forceUpdateTimer);
384
513
  this.forceUpdateTimer = undefined;
@@ -388,6 +517,10 @@ export class ProxyDaemon {
388
517
  this.wakeRunnerPending = false;
389
518
  if (this.wakeRunnerResolve)
390
519
  this.wakeRunnerResolve();
520
+ try {
521
+ await this.publishRecallVerifier.stop();
522
+ }
523
+ catch { /* best-effort verifier shutdown */ }
391
524
  let stopError;
392
525
  try {
393
526
  await this.closeIpc();
@@ -407,7 +540,6 @@ export class ProxyDaemon {
407
540
  catch (err) {
408
541
  stopError = stopError ?? err;
409
542
  }
410
- this.started = false;
411
543
  if (stopError)
412
544
  throw stopError;
413
545
  }
@@ -585,14 +717,22 @@ export class ProxyDaemon {
585
717
  return Number.isFinite(n) && n > 0 ? n : null;
586
718
  }
587
719
  async handleProxyRoute(ctx) {
588
- if (await this.collaborationFacade.handle(ctx))
720
+ const expectedHeader = singleHeader(ctx.req.headers['x-evomap-expected-hub-mode']);
721
+ if (hubModeMismatch(expectedHeader, this.deps.hubMode)) {
722
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
589
723
  return true;
724
+ }
590
725
  const handledAtp = await this.handleAtpRoute(ctx);
591
726
  if (handledAtp)
592
727
  return true;
593
728
  if (ctx.route === 'GET /proxy/status') {
594
729
  ctx.json(200, {
595
730
  running: true,
731
+ status: 'running',
732
+ proxy_protocol_version: PROXY_PROTOCOL_VERSION,
733
+ schema_version: PROXY_STATUS_SCHEMA_VERSION,
734
+ hub_mode: this.deps.hubMode ?? 'public',
735
+ runtime_namespace: this.deps.runtimeNamespace ?? 'default',
596
736
  node_id: this.lifecycle.nodeId ?? null,
597
737
  outbound_pending: this.store.countPending('proxy', this.deps.runtimeNamespace),
598
738
  inbound_pending: this.store.countPending('agent', this.deps.runtimeNamespace) + this.store.countPending('core', this.deps.runtimeNamespace),
@@ -601,22 +741,45 @@ export class ProxyDaemon {
601
741
  hub_auth_status: this.store.getState('hub:auth_status') || null,
602
742
  reauth_backoff_until: this.stateNumber('lifecycle:reauth_until'),
603
743
  hello_rate_limit_until: this.stateNumber('lifecycle:hello_rl_until'),
744
+ publish_recall_verify: this.publishRecallVerifier.status(),
604
745
  });
605
746
  return true;
606
747
  }
607
748
  if (ctx.route === 'POST /mailbox/poll') {
608
- const body = (await ctx.readJson());
609
- const limit = Math.max(1, Math.min(Number(body.limit ?? 10), 50));
610
- const messages = this.store.list({ status: 'pending', limit: 500 })
611
- .filter((m) => (body.type ? m.type === body.type : true))
612
- .filter((m) => (body.direction ? m.direction === body.direction : true))
613
- .slice(0, limit);
749
+ const body = asRecord(await ctx.readJson());
750
+ const limit = boundedRequestLimit(body['limit'], 10, 50);
751
+ if (limit === undefined) {
752
+ ctx.json(400, { error: 'invalid_limit' });
753
+ return true;
754
+ }
755
+ const channel = typeof body['channel'] === 'string' ? body['channel'] : undefined;
756
+ const type = typeof body['type'] === 'string' && body['type'] ? body['type'] : undefined;
757
+ const runtimeNamespace = legacyMailboxRuntimeNamespace(channel, this.deps.runtimeNamespace ?? 'default');
758
+ const messages = runtimeNamespace === undefined
759
+ ? []
760
+ : this.store.list({
761
+ status: 'pending',
762
+ direction: mailboxDirection(body['direction']) ?? 'inbound',
763
+ runtimeNamespace,
764
+ ...(type ? { type } : {}),
765
+ limit,
766
+ }).map(mailbox.legacyMailboxMessage);
614
767
  ctx.json(200, { messages, count: messages.length });
615
768
  return true;
616
769
  }
770
+ if (await this.collaborationFacade.handle(ctx))
771
+ return true;
617
772
  if (ctx.route === 'POST /asset/search') {
618
773
  const body = (await ctx.readJson());
619
- const limit = Math.max(1, Math.min(Number(body.limit ?? 5), 25));
774
+ if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
775
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
776
+ return true;
777
+ }
778
+ const limit = boundedRequestLimit(body.limit, 5, 25);
779
+ if (limit === undefined) {
780
+ ctx.json(400, { error: 'invalid_limit' });
781
+ return true;
782
+ }
620
783
  const rawSignals = Array.isArray(body.signals) ? body.signals : body.signalsAny;
621
784
  const signalsAny = Array.isArray(rawSignals) ? rawSignals.filter((s) => typeof s === 'string') : undefined;
622
785
  const kind = assetKind(body.kind);
@@ -638,6 +801,10 @@ export class ProxyDaemon {
638
801
  }
639
802
  if (ctx.route === 'POST /asset/fetch') {
640
803
  const body = (await ctx.readJson());
804
+ if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
805
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
806
+ return true;
807
+ }
641
808
  const ids = uniqueStrings([
642
809
  ...(Array.isArray(body.asset_ids) ? body.asset_ids : []),
643
810
  ...(typeof body.asset_id === 'string' ? [body.asset_id] : []),
@@ -663,22 +830,81 @@ export class ProxyDaemon {
663
830
  return true;
664
831
  }
665
832
  if (ctx.route === 'POST /asset/submit') {
666
- const body = (await ctx.readJson());
667
- if (!body.assets && !body.asset_id) {
833
+ const body = asRecord(await ctx.readJson());
834
+ if (hubModeMismatch(body['expected_hub_mode'], this.deps.hubMode)) {
835
+ ctx.json(409, { stored: false, error: 'proxy_hub_mode_mismatch' });
836
+ return true;
837
+ }
838
+ const bundle = normalizeAssetSubmitBundle(body);
839
+ const legacyAssetId = typeof body['asset_id'] === 'string' && body['asset_id'].trim()
840
+ ? body['asset_id'].trim()
841
+ : undefined;
842
+ if (!bundle && !legacyAssetId) {
668
843
  ctx.json(400, { error: 'assets or asset_id is required' });
669
844
  return true;
670
845
  }
671
- const env = mailbox.createEnvelope({ type: 'asset_submit', payload: body, now: ctx.now });
846
+ let outboundBundle = bundle;
847
+ if (!outboundBundle && legacyAssetId) {
848
+ if (!this.assetStore) {
849
+ ctx.json(503, { error: 'asset_store_unavailable' });
850
+ return true;
851
+ }
852
+ const resolved = await this.assetStore.get(legacyAssetId);
853
+ if (!resolved) {
854
+ ctx.json(404, { error: 'asset_not_found', asset_id: legacyAssetId });
855
+ return true;
856
+ }
857
+ outboundBundle = [resolved];
858
+ }
859
+ if (outboundBundle && outboundBundle.length > MAX_ASSET_SUBMIT_ITEMS) {
860
+ ctx.json(400, { error: `asset submit accepts at most ${MAX_ASSET_SUBMIT_ITEMS} items` });
861
+ return true;
862
+ }
863
+ const requestedMode = ctx.url.searchParams.get('mode');
864
+ const mode = requestedMode ?? (bundle ? 'sync' : 'async');
865
+ if (mode !== 'sync' && mode !== 'async') {
866
+ ctx.json(400, { error: 'mode must be sync or async' });
867
+ return true;
868
+ }
869
+ if (mode === 'sync') {
870
+ if (!bundle) {
871
+ ctx.json(400, { error: 'mode=sync requires a full asset bundle' });
872
+ return true;
873
+ }
874
+ await this.publishAssetSubmitSynchronously(ctx, outboundBundle);
875
+ return true;
876
+ }
877
+ const payload = {
878
+ assets: outboundBundle,
879
+ [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
880
+ };
881
+ const requestId = typeof body['request_id'] === 'string' && ASYNC_ASSET_SUBMIT_REQUEST_ID.test(body['request_id'])
882
+ ? body['request_id']
883
+ : undefined;
884
+ delete payload['request_id'];
885
+ const runtimeNamespace = this.deps.runtimeNamespace ?? 'default';
886
+ const stableId = requestId ? asyncAssetSubmitEnvelopeId(runtimeNamespace, requestId) : undefined;
887
+ const env = mailbox.createEnvelope({
888
+ ...(stableId ? { id: stableId, idempotencyKey: stableId } : {}),
889
+ type: 'asset_submit',
890
+ payload,
891
+ runtimeNamespace,
892
+ now: ctx.now,
893
+ });
672
894
  const r = this.store.send(env);
673
895
  if (r.stored)
674
896
  this.notifyNewOutbound();
675
- ctx.json(200, { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored });
897
+ ctx.json(202, { id: env.id, message_id: env.id, receiptId: r.receiptId, status: 'pending', stored: r.stored });
676
898
  return true;
677
899
  }
678
900
  if (ctx.route === 'POST /asset/validate') {
679
901
  // Pre-publish dry-run against the hub's quality + content-safety gate (nothing stored, no credits).
680
902
  // Same {assets:[…]} bundle shape as /asset/submit; the adapter wraps it in a GEP-A2A envelope.
681
903
  const body = (await ctx.readJson());
904
+ if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
905
+ ctx.json(409, { valid: false, error: 'proxy_hub_mode_mismatch' });
906
+ return true;
907
+ }
682
908
  const bundle = Array.isArray(body.assets)
683
909
  ? body.assets.filter((a) => Boolean(a && typeof a === 'object'))
684
910
  : (body.asset && typeof body.asset === 'object' && !Array.isArray(body.asset) ? [body.asset] : []);
@@ -704,7 +930,12 @@ export class ProxyDaemon {
704
930
  return true;
705
931
  }
706
932
  if (ctx.route === 'POST /asset/reuse-result') {
707
- const parsed = parseReuseResultReport(await ctx.readJson());
933
+ const body = await ctx.readJson();
934
+ if (hubModeMismatch(asRecord(body)['expected_hub_mode'], this.deps.hubMode)) {
935
+ ctx.json(409, { recorded: false, error: 'proxy_hub_mode_mismatch' });
936
+ return true;
937
+ }
938
+ const parsed = parseReuseResultReport(body);
708
939
  if ('error' in parsed) {
709
940
  ctx.json(400, { recorded: false, error: parsed.error });
710
941
  return true;
@@ -723,6 +954,10 @@ export class ProxyDaemon {
723
954
  }
724
955
  if (ctx.route === 'POST /conversation/distill') {
725
956
  const body = (await ctx.readJson());
957
+ if (hubModeMismatch(body.expected_hub_mode, this.deps.hubMode)) {
958
+ ctx.json(409, { error: 'proxy_hub_mode_mismatch' });
959
+ return true;
960
+ }
726
961
  const distill = await hubNs.distillConversation(body, { persist: body.persist === true, store: this.assetStore });
727
962
  if (!distill.ok) {
728
963
  ctx.json(200, { ...distill, queued: false, submission: null });
@@ -732,7 +967,13 @@ export class ProxyDaemon {
732
967
  if (body['publish'] === true) {
733
968
  const env = mailbox.createEnvelope({
734
969
  type: 'asset_submit',
735
- payload: { source: 'conversation_distillation', distill_id: distill.distill_id, assets: [distill.gene, distill.capsule] },
970
+ payload: {
971
+ source: 'conversation_distillation',
972
+ distill_id: distill.distill_id,
973
+ assets: [distill.gene, distill.capsule],
974
+ [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
975
+ },
976
+ runtimeNamespace: this.deps.runtimeNamespace ?? 'default',
736
977
  now: ctx.now,
737
978
  });
738
979
  const r = this.store.send(env);
@@ -806,7 +1047,7 @@ export class ProxyDaemon {
806
1047
  const localSafe = local.filter((asset) => asset.type !== 'AntiGene');
807
1048
  let remote;
808
1049
  try {
809
- remote = (await this.deps.hub.search(query)).filter((asset) => asset.type !== 'AntiGene');
1050
+ remote = await this.searchRemoteAssets(query, limit);
810
1051
  }
811
1052
  catch (error) {
812
1053
  if (localSafe.length === 0)
@@ -830,6 +1071,364 @@ export class ProxyDaemon {
830
1071
  }
831
1072
  return out;
832
1073
  }
1074
+ async searchRemoteAssets(query, limit) {
1075
+ const key = assetSearchCacheKey(this.deps.runtimeNamespace, query, limit);
1076
+ const now = this.now();
1077
+ const cached = this.assetSearchCache.get(key);
1078
+ if (cached && cached.expiresAt > now)
1079
+ return cached.value;
1080
+ if (now < this.assetSearchCooldownUntil) {
1081
+ if (cached && cached.staleUntil > now)
1082
+ return cached.value;
1083
+ if (cached)
1084
+ this.assetSearchCache.delete(key);
1085
+ throw new HubClientError(429, { error: 'rate_limited', source: 'asset_search_client_cooldown' }, this.assetSearchCooldownUntil - now);
1086
+ }
1087
+ const inflight = this.assetSearchInflight.get(key);
1088
+ if (inflight)
1089
+ return inflight;
1090
+ const request = (async () => {
1091
+ try {
1092
+ const value = (await this.deps.hub.search(query))
1093
+ .filter((asset) => asset.type !== 'AntiGene')
1094
+ .slice(0, limit);
1095
+ this.cacheRemoteAssetSearch(key, value, this.now());
1096
+ return value;
1097
+ }
1098
+ catch (error) {
1099
+ const retryAfterMs = assetSearchRetryAfterMs(error, this.assetSearchCacheTtlMs);
1100
+ if (retryAfterMs !== undefined) {
1101
+ const rateLimitedAt = this.now();
1102
+ this.assetSearchCooldownUntil = Math.max(this.assetSearchCooldownUntil, rateLimitedAt + retryAfterMs);
1103
+ const stale = this.assetSearchCache.get(key);
1104
+ if (stale && stale.staleUntil > rateLimitedAt)
1105
+ return stale.value;
1106
+ }
1107
+ throw error;
1108
+ }
1109
+ })();
1110
+ this.assetSearchInflight.set(key, request);
1111
+ const clearInflight = () => {
1112
+ if (this.assetSearchInflight.get(key) === request)
1113
+ this.assetSearchInflight.delete(key);
1114
+ };
1115
+ void request.then(clearInflight, clearInflight);
1116
+ return request;
1117
+ }
1118
+ cacheRemoteAssetSearch(key, value, now) {
1119
+ if (this.assetSearchCache.size >= this.assetSearchCacheMax && !this.assetSearchCache.has(key)) {
1120
+ const oldest = this.assetSearchCache.keys().next().value;
1121
+ if (oldest !== undefined)
1122
+ this.assetSearchCache.delete(oldest);
1123
+ }
1124
+ this.assetSearchCache.delete(key);
1125
+ this.assetSearchCache.set(key, {
1126
+ value,
1127
+ expiresAt: now + this.assetSearchCacheTtlMs,
1128
+ staleUntil: now + this.assetSearchCacheTtlMs + this.assetSearchStaleGraceMs,
1129
+ });
1130
+ }
1131
+ async publishAssetSubmitSynchronously(ctx, items) {
1132
+ const classified = classifySynchronousAssetSubmit(items);
1133
+ if (!classified.ok) {
1134
+ ctx.json(422, { error: classified.error, code: 'invalid_asset_submit' });
1135
+ return;
1136
+ }
1137
+ if (classified.kind === 'wire') {
1138
+ const envelope = this.createSynchronousAssetSubmitEnvelope(classified.bundle, undefined, ctx.now);
1139
+ this.writeSynchronousAssetSubmitOutcome(ctx, await this.publishSynchronousBundle(envelope));
1140
+ return;
1141
+ }
1142
+ const results = [];
1143
+ for (const item of classified.items) {
1144
+ const converted = await convertLegacyLooseAsset(item);
1145
+ if (!converted.ok) {
1146
+ results.push({ ok: false, error: converted.error, statusCode: 422 });
1147
+ continue;
1148
+ }
1149
+ const envelope = this.createSynchronousAssetSubmitEnvelope(converted.bundle, 'v1_loose_asset_compat', ctx.now);
1150
+ const outcome = await this.publishSynchronousBundle(envelope);
1151
+ if (outcome.kind === 'accepted') {
1152
+ const receipt = outcome.receipt;
1153
+ const publishedIds = submittedAssetIds(receipt, converted.bundle);
1154
+ results.push({
1155
+ ok: true,
1156
+ gene_asset_id: publishedIds[0],
1157
+ capsule_asset_id: publishedIds[1],
1158
+ response: receipt,
1159
+ });
1160
+ }
1161
+ else if (outcome.kind === 'failed') {
1162
+ results.push({
1163
+ ok: false,
1164
+ error: String(outcome.body['error']),
1165
+ statusCode: outcome.statusCode,
1166
+ ...(typeof outcome.body['reason'] === 'string' ? { reason: outcome.body['reason'] } : {}),
1167
+ });
1168
+ }
1169
+ else {
1170
+ results.push({
1171
+ ok: false,
1172
+ error: 'publish_pending',
1173
+ statusCode: 202,
1174
+ reason: `durable recovery pending (${outcome.messageId})`,
1175
+ });
1176
+ }
1177
+ }
1178
+ ctx.json(200, {
1179
+ published: results.filter((result) => result.ok).length,
1180
+ total: results.length,
1181
+ results,
1182
+ });
1183
+ }
1184
+ createSynchronousAssetSubmitEnvelope(bundle, source, now) {
1185
+ const canonicalBundle = [...bundle].sort(compareSynchronousAssetSubmitAssets);
1186
+ const runtimeNamespace = this.deps.runtimeNamespace ?? 'default';
1187
+ const idempotencyKey = synchronousAssetSubmitKey(this.synchronousAssetSubmitScope, runtimeNamespace, this.currentHubMode(), canonicalBundle);
1188
+ return mailbox.createEnvelope({
1189
+ id: `compat:asset_submit:${idempotencyKey.slice(SYNC_ASSET_SUBMIT_PREFIX.length)}`,
1190
+ type: 'asset_submit',
1191
+ payload: {
1192
+ ...(source ? { source } : {}),
1193
+ assets: canonicalBundle,
1194
+ [OUTBOUND_HUB_MODE_FIELD]: this.currentHubMode(),
1195
+ },
1196
+ idempotencyKey,
1197
+ runtimeNamespace,
1198
+ now,
1199
+ });
1200
+ }
1201
+ currentHubMode() {
1202
+ return this.deps.hubMode ?? 'public';
1203
+ }
1204
+ handleHubModeBoundOutbound(envelope) {
1205
+ if (envelope.type !== 'asset_submit')
1206
+ return this.handleSynchronousProxyOutbound(envelope);
1207
+ const payload = asRecord(envelope.payload);
1208
+ const rawQueuedMode = payload[OUTBOUND_HUB_MODE_FIELD];
1209
+ const queuedMode = rawQueuedMode === undefined ? 'public' : String(rawQueuedMode);
1210
+ const currentMode = this.currentHubMode();
1211
+ if ((queuedMode !== 'public' && queuedMode !== 'private') || queuedMode !== currentMode) {
1212
+ throw new OutboundHubModeMismatchError(queuedMode, currentMode);
1213
+ }
1214
+ const outboundPayload = { ...payload };
1215
+ delete outboundPayload[OUTBOUND_HUB_MODE_FIELD];
1216
+ return this.handleSynchronousProxyOutbound({ ...envelope, payload: outboundPayload });
1217
+ }
1218
+ async publishSynchronousBundle(envelope) {
1219
+ const cached = this.readSynchronousAssetSubmitOutcome(envelope);
1220
+ if (cached)
1221
+ return cached;
1222
+ const inflight = this.synchronousAssetSubmitInflight.get(envelope.idempotencyKey);
1223
+ if (inflight)
1224
+ return this.waitForSynchronousAssetSubmit(inflight, envelope.id);
1225
+ const { stored } = this.store.send(envelope);
1226
+ const cachedAfterInsert = this.readSynchronousAssetSubmitOutcome(envelope);
1227
+ if (cachedAfterInsert)
1228
+ return cachedAfterInsert;
1229
+ if (!stored) {
1230
+ const existingInflight = this.synchronousAssetSubmitInflight.get(envelope.idempotencyKey);
1231
+ return existingInflight
1232
+ ? this.waitForSynchronousAssetSubmit(existingInflight, envelope.id)
1233
+ : { kind: 'pending', messageId: envelope.id };
1234
+ }
1235
+ this.store.defer(envelope.id, 'synchronous asset submit attempt in progress', this.now(), SYNC_ASSET_SUBMIT_DIRECT_RETRY_GRACE_MS);
1236
+ this.notifyNewOutbound();
1237
+ const request = this.executeSynchronousAssetSubmit(envelope);
1238
+ this.synchronousAssetSubmitInflight.set(envelope.idempotencyKey, request);
1239
+ void request.finally(() => {
1240
+ if (this.synchronousAssetSubmitInflight.get(envelope.idempotencyKey) === request) {
1241
+ this.synchronousAssetSubmitInflight.delete(envelope.idempotencyKey);
1242
+ }
1243
+ }).catch(() => { });
1244
+ return this.waitForSynchronousAssetSubmit(request, envelope.id);
1245
+ }
1246
+ waitForSynchronousAssetSubmit(request, messageId) {
1247
+ return new Promise((resolve, reject) => {
1248
+ const timeout = setTimeout(() => {
1249
+ resolve({ kind: 'pending', messageId });
1250
+ }, this.assetSubmitResponseTimeoutMs);
1251
+ timeout.unref?.();
1252
+ void request.then((outcome) => {
1253
+ clearTimeout(timeout);
1254
+ resolve(outcome);
1255
+ }, (error) => {
1256
+ clearTimeout(timeout);
1257
+ reject(error);
1258
+ });
1259
+ });
1260
+ }
1261
+ async executeSynchronousAssetSubmit(envelope) {
1262
+ try {
1263
+ const receipt = await this.proxyHandler(envelope);
1264
+ const firstObservation = this.cacheSynchronousAssetSubmitSuccess(envelope, receipt);
1265
+ if (firstObservation) {
1266
+ try {
1267
+ this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
1268
+ }
1269
+ catch { /* best-effort */ }
1270
+ }
1271
+ if (this.store.getById(envelope.id)?.status !== 'in_flight')
1272
+ this.store.complete(envelope.id, this.now());
1273
+ return { kind: 'accepted', receipt };
1274
+ }
1275
+ catch (error) {
1276
+ const current = this.store.getById(envelope.id);
1277
+ const currentStatus = this.store.getStatus(envelope.id);
1278
+ const cached = this.readSynchronousAssetSubmitOutcome(envelope);
1279
+ // Once acceptance is durable, later publish or local-finalization errors cannot turn the external outcome
1280
+ // into a failure. A late acceptance may also need to recover an intent that a racing rejection put in DLQ.
1281
+ if (cached?.kind === 'accepted') {
1282
+ if (currentStatus?.dlq) {
1283
+ try {
1284
+ this.store.replayDlq(envelope.id, this.now());
1285
+ this.notifyNewOutbound();
1286
+ }
1287
+ catch (recoveryError) {
1288
+ this.recordTickError('outbound', recoveryError);
1289
+ }
1290
+ }
1291
+ return cached;
1292
+ }
1293
+ const failure = mapSynchronousPublishFailure(error);
1294
+ const outcome = { kind: 'failed', ...failure, error };
1295
+ if (this.shadowMode)
1296
+ return outcome;
1297
+ if (current?.status !== 'in_flight') {
1298
+ const message = safeDaemonMessage(JSON.stringify(failure.body), MAX_PROXY_TICK_ERROR_LENGTH);
1299
+ const outcomeKey = synchronousAssetSubmitOutcomeKey(envelope.idempotencyKey);
1300
+ const acceptedKey = synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey);
1301
+ const transitionNow = this.now();
1302
+ const terminal = isTerminalSynchronousPublishFailure(error);
1303
+ const transitioned = terminal
1304
+ ? this.store.failAndMarkProcessedUnlessProcessed(envelope.id, [acceptedKey], outcomeKey, { kind: 'failed', ...failure }, message, transitionNow, 1)
1305
+ : isRetryableSynchronousPublishFailure(error)
1306
+ ? this.store.deferUnlessProcessed(envelope.id, acceptedKey, message, transitionNow, synchronousPublishRetryAfterMs(error, failure))
1307
+ : this.store.failUnlessProcessed(envelope.id, acceptedKey, message, transitionNow);
1308
+ if (!transitioned) {
1309
+ const persisted = this.readSynchronousAssetSubmitOutcome(envelope);
1310
+ if (persisted)
1311
+ return persisted;
1312
+ }
1313
+ if (terminal) {
1314
+ const persisted = this.readSynchronousAssetSubmitOutcome(envelope);
1315
+ if (persisted?.kind === 'accepted')
1316
+ return persisted;
1317
+ }
1318
+ this.notifyNewOutbound();
1319
+ }
1320
+ return outcome;
1321
+ }
1322
+ }
1323
+ async handleSynchronousProxyOutbound(envelope) {
1324
+ if (!isSynchronousAssetSubmitEnvelope(envelope))
1325
+ return this.proxyHandler(envelope);
1326
+ const cached = this.readSynchronousAssetSubmitOutcome(envelope);
1327
+ if (cached?.kind === 'accepted')
1328
+ return cached.receipt;
1329
+ if (cached?.kind === 'failed')
1330
+ throw cachedSynchronousAssetSubmitFailure(cached);
1331
+ const inflight = this.synchronousAssetSubmitInflight.get(envelope.idempotencyKey);
1332
+ if (inflight) {
1333
+ const outcome = await this.waitForSynchronousAssetSubmit(inflight, envelope.id);
1334
+ if (outcome.kind === 'accepted')
1335
+ return outcome.receipt;
1336
+ if (outcome.kind === 'failed')
1337
+ throw outcome.error ?? cachedSynchronousAssetSubmitFailure(outcome);
1338
+ if (this.synchronousAssetSubmitInflight.get(envelope.idempotencyKey) === inflight) {
1339
+ this.synchronousAssetSubmitInflight.delete(envelope.idempotencyKey);
1340
+ }
1341
+ throw new HubUnreachableError('synchronous asset submit is still pending');
1342
+ }
1343
+ try {
1344
+ const receipt = await this.proxyHandler(envelope);
1345
+ const firstObservation = this.cacheSynchronousAssetSubmitSuccess(envelope, receipt);
1346
+ if (firstObservation) {
1347
+ try {
1348
+ this.publishRecallVerifier.observeAcceptedPublish(envelope, receipt);
1349
+ }
1350
+ catch { /* best-effort */ }
1351
+ }
1352
+ return receipt;
1353
+ }
1354
+ catch (error) {
1355
+ const cached = this.readSynchronousAssetSubmitOutcome(envelope);
1356
+ if (cached?.kind === 'accepted')
1357
+ return cached.receipt;
1358
+ if (isTerminalSynchronousPublishFailure(error))
1359
+ this.cacheSynchronousAssetSubmitTerminal(envelope, error);
1360
+ throw error;
1361
+ }
1362
+ }
1363
+ readSynchronousAssetSubmitOutcome(envelope) {
1364
+ if (this.shadowMode || !isSynchronousAssetSubmitEnvelope(envelope))
1365
+ return undefined;
1366
+ const outcomeKey = synchronousAssetSubmitOutcomeKey(envelope.idempotencyKey);
1367
+ const value = asRecord(this.store.getProcessed(outcomeKey));
1368
+ if (value['kind'] === 'accepted' && Object.prototype.hasOwnProperty.call(value, 'receipt')) {
1369
+ const receipt = asRecord(value['receipt']);
1370
+ if (receipt['bundleId'] === 'shadow-bundle'
1371
+ && typeof receipt['receiptId'] === 'string'
1372
+ && receipt['receiptId'].startsWith('shadow-')) {
1373
+ this.store.deleteProcessed([
1374
+ outcomeKey,
1375
+ synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey),
1376
+ ]);
1377
+ return undefined;
1378
+ }
1379
+ const backfilled = this.store.markProcessedIf(outcomeKey, synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey), { accepted: true }, this.now(), (current) => {
1380
+ const record = asRecord(current);
1381
+ return record['kind'] === 'accepted' && Object.prototype.hasOwnProperty.call(record, 'receipt');
1382
+ });
1383
+ if (!backfilled)
1384
+ return this.readSynchronousAssetSubmitOutcome(envelope);
1385
+ return { kind: 'accepted', receipt: value['receipt'] };
1386
+ }
1387
+ if (value['kind'] === 'failed') {
1388
+ const statusCode = positiveFiniteNumber(value['statusCode']);
1389
+ if (statusCode !== undefined && isRecordValue(value['body'])) {
1390
+ return { kind: 'failed', statusCode, body: value['body'] };
1391
+ }
1392
+ }
1393
+ return undefined;
1394
+ }
1395
+ cacheSynchronousAssetSubmitSuccess(envelope, receipt) {
1396
+ if (this.shadowMode || !isSynchronousAssetSubmitEnvelope(envelope))
1397
+ return undefined;
1398
+ const key = synchronousAssetSubmitOutcomeKey(envelope.idempotencyKey);
1399
+ const acceptedKey = synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey);
1400
+ const cached = this.readSynchronousAssetSubmitOutcome(envelope);
1401
+ if (cached?.kind === 'accepted')
1402
+ return false;
1403
+ // Acceptance is monotonic: a concurrent attempt may reject after another request reached the Hub, but a
1404
+ // real acceptance must supersede an earlier rejection so replay reflects the economic side effect.
1405
+ this.store.replaceProcessedWithMarker(key, { kind: 'accepted', receipt }, acceptedKey, { accepted: true }, this.now());
1406
+ return true;
1407
+ }
1408
+ cacheSynchronousAssetSubmitTerminal(envelope, error) {
1409
+ if (this.shadowMode || !isSynchronousAssetSubmitEnvelope(envelope))
1410
+ return;
1411
+ if (this.store.isProcessed(synchronousAssetSubmitAcceptanceKey(envelope.idempotencyKey)))
1412
+ return;
1413
+ if (this.readSynchronousAssetSubmitOutcome(envelope)?.kind === 'accepted')
1414
+ return;
1415
+ const failure = mapSynchronousPublishFailure(error);
1416
+ this.store.markProcessed(synchronousAssetSubmitOutcomeKey(envelope.idempotencyKey), {
1417
+ kind: 'failed',
1418
+ ...failure,
1419
+ }, this.now());
1420
+ }
1421
+ writeSynchronousAssetSubmitOutcome(ctx, outcome) {
1422
+ if (outcome.kind === 'accepted') {
1423
+ ctx.json(200, outcome.receipt);
1424
+ }
1425
+ else if (outcome.kind === 'failed') {
1426
+ ctx.json(outcome.statusCode, outcome.body);
1427
+ }
1428
+ else {
1429
+ ctx.json(202, { status: 'pending', message_id: outcome.messageId, durable: true });
1430
+ }
1431
+ }
833
1432
  async handleAtpRoute(ctx) {
834
1433
  if (!ctx.url.pathname.startsWith('/atp/'))
835
1434
  return false;
@@ -929,7 +1528,17 @@ function isValidator(value) {
929
1528
  return Boolean(value && typeof value === 'object' && typeof value.validate === 'function');
930
1529
  }
931
1530
  function assetMatchesId(asset, assetId) {
932
- return Boolean(asset && asset.asset_id === assetId);
1531
+ if (!asset)
1532
+ return false;
1533
+ return assetId.startsWith('sha256:')
1534
+ ? asset.asset_id === assetId
1535
+ : asset.asset_id === assetId || asset['id'] === assetId;
1536
+ }
1537
+ function hubModeMismatch(expected, actual) {
1538
+ return expected !== undefined && expected !== (actual ?? 'public');
1539
+ }
1540
+ function singleHeader(value) {
1541
+ return Array.isArray(value) ? value[0] : value;
933
1542
  }
934
1543
  function uniqueStrings(values) {
935
1544
  const seen = new Set();
@@ -942,12 +1551,446 @@ function uniqueStrings(values) {
942
1551
  }
943
1552
  return out;
944
1553
  }
1554
+ function assetSearchCacheKey(runtimeNamespace, query, limit) {
1555
+ return JSON.stringify({
1556
+ runtimeNamespace: runtimeNamespace ?? 'default',
1557
+ kind: query.kind ?? null,
1558
+ signalsAny: uniqueStrings(query.signalsAny ?? []).sort(),
1559
+ category: query.category ?? null,
1560
+ gene: query.gene ?? null,
1561
+ text: query.text ?? null,
1562
+ limit,
1563
+ });
1564
+ }
1565
+ function assetSearchRetryAfterMs(error, fallbackMs) {
1566
+ const structured = asRecord(error);
1567
+ const details = asRecord(structured['details']);
1568
+ const structuredStatus = structured['statusCode']
1569
+ ?? structured['status']
1570
+ ?? details['statusCode']
1571
+ ?? details['status'];
1572
+ const status = error instanceof HubClientError
1573
+ ? error.status
1574
+ : (typeof structuredStatus === 'number'
1575
+ ? structuredStatus
1576
+ : (typeof structuredStatus === 'string' ? Number(structuredStatus) : NaN));
1577
+ if (status !== 429)
1578
+ return undefined;
1579
+ const body = asRecord(error instanceof HubClientError ? error.body : structured['body']);
1580
+ const retryAfterMs = positiveFiniteNumber(error instanceof HubClientError
1581
+ ? error.retryAfterMs
1582
+ : structured['retryAfterMs'] ?? details['retryAfterMs']) ?? positiveFiniteNumber(body['retry_after_ms'] ?? body['retryAfterMs']);
1583
+ const retryAfterSeconds = positiveFiniteNumber(body['retry_after'] ?? body['retryAfter']);
1584
+ return Math.floor(Math.min(retryAfterMs ?? (retryAfterSeconds !== undefined ? retryAfterSeconds * 1_000 : fallbackMs), MAX_TIMER_DELAY_MS));
1585
+ }
1586
+ function positiveFiniteNumber(value) {
1587
+ const parsed = typeof value === 'number'
1588
+ ? value
1589
+ : (typeof value === 'string' && value.trim().length > 0 ? Number(value) : NaN);
1590
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
1591
+ }
1592
+ function positiveIntegerOr(value, fallback) {
1593
+ const parsed = positiveFiniteNumber(value);
1594
+ return parsed === undefined ? fallback : Math.max(1, Math.floor(parsed));
1595
+ }
1596
+ function boundedRequestLimit(value, fallback, maximum) {
1597
+ if (value === undefined)
1598
+ return fallback;
1599
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0)
1600
+ return undefined;
1601
+ return Math.min(value, maximum);
1602
+ }
1603
+ function legacyMailboxRuntimeNamespace(requestedChannel, runtimeNamespace) {
1604
+ if (requestedChannel === undefined || requestedChannel === 'evomap-hub' || requestedChannel === runtimeNamespace) {
1605
+ return runtimeNamespace;
1606
+ }
1607
+ return undefined;
1608
+ }
1609
+ function mailboxDirection(value) {
1610
+ return value === 'inbound' || value === 'outbound' || value === 'local' ? value : undefined;
1611
+ }
945
1612
  function assetKind(value) {
946
1613
  return value === 'Gene' || value === 'Capsule' || value === 'EvolutionEvent' || value === 'AntiGene' ? value : undefined;
947
1614
  }
948
1615
  function asRecord(value) {
949
1616
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
950
1617
  }
1618
+ function normalizeAssetSubmitBundle(body) {
1619
+ if (Object.prototype.hasOwnProperty.call(body, 'assets')) {
1620
+ const assets = body['assets'];
1621
+ return Array.isArray(assets) && assets.length > 0 && assets.every(isNonEmptyAssetRecord)
1622
+ ? assets
1623
+ : null;
1624
+ }
1625
+ return isNonEmptyAssetRecord(body['asset']) ? [body['asset']] : null;
1626
+ }
1627
+ function isNonEmptyAssetRecord(value) {
1628
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0);
1629
+ }
1630
+ function classifySynchronousAssetSubmit(items) {
1631
+ const wireLooking = items.map(isWireLookingAsset);
1632
+ const legacyLoose = items.map(isClearlyLegacyLooseAsset);
1633
+ if (wireLooking.every(Boolean)) {
1634
+ const bundle = [];
1635
+ for (let index = 0; index < items.length; index += 1) {
1636
+ const item = items[index];
1637
+ if (!wire.validateWire(item).ok) {
1638
+ return { ok: false, error: `asset ${index}: malformed V2 wire asset` };
1639
+ }
1640
+ try {
1641
+ const normalized = assetstore.normalizeForPut(item);
1642
+ if (!normalized.verified) {
1643
+ return { ok: false, error: `asset ${index}: a verified content-addressed asset_id is required` };
1644
+ }
1645
+ bundle.push(normalized.record);
1646
+ }
1647
+ catch {
1648
+ return { ok: false, error: `asset ${index}: asset_id does not match its content` };
1649
+ }
1650
+ }
1651
+ return { ok: true, kind: 'wire', bundle };
1652
+ }
1653
+ if (legacyLoose.every(Boolean))
1654
+ return { ok: true, kind: 'legacy', items };
1655
+ if (wireLooking.some(Boolean) && legacyLoose.some(Boolean)) {
1656
+ return { ok: false, error: 'wire assets and legacy loose assets cannot be mixed in one request' };
1657
+ }
1658
+ if (wireLooking.some(Boolean)) {
1659
+ return { ok: false, error: 'all wire-looking items must be valid content-addressed V2 assets' };
1660
+ }
1661
+ return { ok: false, error: 'unsupported asset input; provide V2 wire assets or legacy content/summary/strategy' };
1662
+ }
1663
+ function isWireLookingAsset(value) {
1664
+ return Object.prototype.hasOwnProperty.call(value, 'schema_version')
1665
+ || Object.prototype.hasOwnProperty.call(value, 'asset_id');
1666
+ }
1667
+ function isClearlyLegacyLooseAsset(value) {
1668
+ if (Object.prototype.hasOwnProperty.call(value, 'schema_version')
1669
+ || Object.prototype.hasOwnProperty.call(value, 'asset_id'))
1670
+ return false;
1671
+ return ['content', 'summary', 'strategy'].some((key) => Object.prototype.hasOwnProperty.call(value, key));
1672
+ }
1673
+ async function convertLegacyLooseAsset(value) {
1674
+ const normalized = legacyLooseDistillInput(value);
1675
+ if (!normalized.ok)
1676
+ return normalized;
1677
+ try {
1678
+ const distilled = await hubNs.distillConversation(normalized.input, { persist: false });
1679
+ if (!distilled.ok) {
1680
+ return { ok: false, error: `legacy_distill_${safeIdentifier(distilled.reason)}` };
1681
+ }
1682
+ const gene = {
1683
+ ...distilled.gene,
1684
+ ...(normalized.constraints
1685
+ ? { constraints: mergeLegacyConstraints(distilled.gene['constraints'], normalized.constraints) }
1686
+ : {}),
1687
+ ...(normalized.category ? { category: normalized.category } : {}),
1688
+ };
1689
+ const bundle = [gene, distilled.capsule].map(deterministicDistilledAsset);
1690
+ if (!bundle.every((asset) => wire.validateWire(asset).ok)) {
1691
+ return { ok: false, error: 'legacy_distill_invalid_wire_output' };
1692
+ }
1693
+ return { ok: true, bundle };
1694
+ }
1695
+ catch {
1696
+ return { ok: false, error: 'legacy_distill_failed' };
1697
+ }
1698
+ }
1699
+ function legacyLooseDistillInput(value) {
1700
+ const content = strictOptionalString(value, 'content');
1701
+ const summary = strictOptionalString(value, 'summary');
1702
+ if (!content.ok || !summary.ok)
1703
+ return { ok: false, error: 'legacy content and summary must be strings' };
1704
+ const strategy = strictOptionalStringList(value, 'strategy', 10, 220);
1705
+ if (!strategy.ok)
1706
+ return { ok: false, error: 'legacy strategy must be an array of strings' };
1707
+ if (strategy.value && (strategy.value.length < 2 || strategy.value.some((step) => step.length < 15))) {
1708
+ return { ok: false, error: 'legacy strategy requires at least two steps of 15 characters each' };
1709
+ }
1710
+ const text = [content.value, summary.value].filter(Boolean).join('\n').trim();
1711
+ const suppliedSubstance = [text, ...(strategy.value ?? [])].join(' ').trim();
1712
+ if (!strategy.value && text.length < 50) {
1713
+ return { ok: false, error: 'legacy content or summary must contain at least 50 characters' };
1714
+ }
1715
+ if (suppliedSubstance.length < 50) {
1716
+ return { ok: false, error: 'legacy input does not contain enough substantive content' };
1717
+ }
1718
+ const signals = strictOptionalStringList(value, 'signals', 12, 64);
1719
+ const signalsMatch = strictOptionalStringList(value, 'signals_match', 12, 64);
1720
+ const validation = strictOptionalStringList(value, 'validation', 8, 180);
1721
+ const verification = strictOptionalStringList(value, 'verification', 8, 180);
1722
+ const artifacts = strictOptionalStringList(value, 'artifacts', 12, 240);
1723
+ if (!signals.ok || !signalsMatch.ok || !validation.ok || !verification.ok || !artifacts.ok) {
1724
+ return { ok: false, error: 'legacy list fields must contain strings only' };
1725
+ }
1726
+ const constraints = parseLegacyConstraints(value['constraints']);
1727
+ if (!constraints.ok)
1728
+ return constraints;
1729
+ const category = parseLegacyCategory(value['category']);
1730
+ if (!category.ok)
1731
+ return category;
1732
+ const derivedSummary = summary.value
1733
+ || content.value?.slice(0, 300)
1734
+ || strategy.value?.join('; ').slice(0, 300)
1735
+ || '';
1736
+ const input = {
1737
+ summary: derivedSummary,
1738
+ transcript: content.value ?? derivedSummary,
1739
+ ...(strategy.value ? { strategy: strategy.value } : {}),
1740
+ ...((signals.value ?? signalsMatch.value) ? { signals: signals.value ?? signalsMatch.value } : {}),
1741
+ ...((validation.value ?? verification.value) ? { validation: validation.value ?? verification.value } : {}),
1742
+ ...(artifacts.value ? { artifacts: artifacts.value } : {}),
1743
+ ...strictForwardString(value, 'title'),
1744
+ ...strictForwardString(value, 'name'),
1745
+ ...strictForwardString(value, 'platform'),
1746
+ ...strictForwardString(value, 'model'),
1747
+ ...strictForwardString(value, 'thread_id'),
1748
+ ...(isRecordValue(value['execution']) ? { execution: value['execution'] } : {}),
1749
+ ...(isRecordValue(value['blast_radius']) ? { blast_radius: value['blast_radius'] } : {}),
1750
+ // Compatibility callers may not lower the V2 quality gate.
1751
+ min_score: 5,
1752
+ persist: false,
1753
+ };
1754
+ return {
1755
+ ok: true,
1756
+ input,
1757
+ ...(constraints.value ? { constraints: constraints.value } : {}),
1758
+ ...(category.value ? { category: category.value } : {}),
1759
+ };
1760
+ }
1761
+ function parseLegacyConstraints(value) {
1762
+ if (value === undefined)
1763
+ return { ok: true };
1764
+ if (!isRecordValue(value))
1765
+ return { ok: false, error: 'legacy constraints must be an object' };
1766
+ if (Object.keys(value).some((key) => key !== 'max_files' && key !== 'forbidden_paths')) {
1767
+ return { ok: false, error: 'legacy constraints contains unsupported fields' };
1768
+ }
1769
+ const maxFiles = value['max_files'];
1770
+ if (maxFiles !== undefined && (!Number.isInteger(maxFiles) || Number(maxFiles) < 1 || Number(maxFiles) > 10_000)) {
1771
+ return { ok: false, error: 'legacy constraints.max_files must be an integer from 1 to 10000' };
1772
+ }
1773
+ const forbiddenPaths = value['forbidden_paths'];
1774
+ if (forbiddenPaths !== undefined && (!Array.isArray(forbiddenPaths)
1775
+ || forbiddenPaths.length > 50
1776
+ || forbiddenPaths.some((path) => typeof path !== 'string' || path.trim().length === 0 || path.trim().length > 200))) {
1777
+ return { ok: false, error: 'legacy constraints.forbidden_paths must be a bounded string array' };
1778
+ }
1779
+ const normalizedPaths = Array.isArray(forbiddenPaths)
1780
+ ? uniqueStrings(forbiddenPaths.map((path) => String(path).trim()))
1781
+ : undefined;
1782
+ return {
1783
+ ok: true,
1784
+ value: {
1785
+ ...(typeof maxFiles === 'number' ? { max_files: maxFiles } : {}),
1786
+ ...(normalizedPaths ? { forbidden_paths: normalizedPaths } : {}),
1787
+ },
1788
+ };
1789
+ }
1790
+ function mergeLegacyConstraints(base, legacy) {
1791
+ const current = isRecordValue(base) ? base : {};
1792
+ const currentMax = Number.isInteger(current['max_files']) && Number(current['max_files']) > 0
1793
+ ? Number(current['max_files'])
1794
+ : 20;
1795
+ const currentPaths = Array.isArray(current['forbidden_paths'])
1796
+ ? current['forbidden_paths'].filter((path) => typeof path === 'string')
1797
+ : [];
1798
+ return {
1799
+ max_files: Math.min(currentMax, legacy.max_files ?? currentMax),
1800
+ forbidden_paths: uniqueStrings([...currentPaths, ...(legacy.forbidden_paths ?? [])]),
1801
+ };
1802
+ }
1803
+ function parseLegacyCategory(value) {
1804
+ if (value === undefined)
1805
+ return { ok: true };
1806
+ if (value === 'repair' || value === 'optimize' || value === 'innovate' || value === 'explore') {
1807
+ return { ok: true, value };
1808
+ }
1809
+ return { ok: false, error: 'legacy category is invalid' };
1810
+ }
1811
+ function deterministicDistilledAsset(asset) {
1812
+ const draft = { ...asset, asset_id: '' };
1813
+ // `_source` is local distiller provenance and is not part of the current GEP Gene schema.
1814
+ // It also contains a wall-clock timestamp, so it must not influence compatibility asset ids.
1815
+ delete draft['_source'];
1816
+ return assetstore.normalizeForPut(draft).record;
1817
+ }
1818
+ function strictOptionalString(value, key) {
1819
+ if (!Object.prototype.hasOwnProperty.call(value, key))
1820
+ return { ok: true };
1821
+ const raw = value[key];
1822
+ if (typeof raw !== 'string')
1823
+ return { ok: false };
1824
+ const trimmed = raw.trim();
1825
+ return { ok: true, ...(trimmed ? { value: trimmed } : {}) };
1826
+ }
1827
+ function strictOptionalStringList(value, key, maxItems, maxLength) {
1828
+ if (!Object.prototype.hasOwnProperty.call(value, key))
1829
+ return { ok: true };
1830
+ const raw = value[key];
1831
+ if (!Array.isArray(raw) || raw.some((item) => typeof item !== 'string'))
1832
+ return { ok: false };
1833
+ const normalized = raw.map((item) => item.trim()).filter(Boolean).slice(0, maxItems);
1834
+ if (normalized.some((item) => item.length > maxLength))
1835
+ return { ok: false };
1836
+ return { ok: true, ...(normalized.length > 0 ? { value: normalized } : {}) };
1837
+ }
1838
+ function strictForwardString(value, key) {
1839
+ const parsed = strictOptionalString(value, key);
1840
+ return parsed.ok && parsed.value ? { [key]: parsed.value } : {};
1841
+ }
1842
+ function isRecordValue(value) {
1843
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
1844
+ }
1845
+ function safeIdentifier(value) {
1846
+ return value.replace(/[^a-z0-9_]+/gi, '_').slice(0, 80) || 'rejected';
1847
+ }
1848
+ function synchronousAssetSubmitKey(scope, runtimeNamespace, hubMode, bundle) {
1849
+ const assetIds = bundle.map((asset) => asset.asset_id).sort();
1850
+ const digestInput = hubMode === 'private'
1851
+ ? [scope, runtimeNamespace, hubMode, assetIds]
1852
+ : [scope, runtimeNamespace, assetIds];
1853
+ const digest = createHash('sha256')
1854
+ .update(JSON.stringify(digestInput))
1855
+ .digest('hex');
1856
+ return `${SYNC_ASSET_SUBMIT_PREFIX}${digest}`;
1857
+ }
1858
+ function asyncAssetSubmitEnvelopeId(runtimeNamespace, requestId) {
1859
+ const digest = createHash('sha256')
1860
+ .update(JSON.stringify([runtimeNamespace, requestId]))
1861
+ .digest('hex');
1862
+ return `${ASYNC_ASSET_SUBMIT_PREFIX}${digest}`;
1863
+ }
1864
+ function compareSynchronousAssetSubmitAssets(left, right) {
1865
+ const typeOrder = SYNC_ASSET_SUBMIT_TYPE_RANK[left.type] - SYNC_ASSET_SUBMIT_TYPE_RANK[right.type];
1866
+ if (typeOrder !== 0)
1867
+ return typeOrder;
1868
+ if (left.asset_id < right.asset_id)
1869
+ return -1;
1870
+ if (left.asset_id > right.asset_id)
1871
+ return 1;
1872
+ return 0;
1873
+ }
1874
+ function synchronousAssetSubmitOutcomeKey(idempotencyKey) {
1875
+ return `${idempotencyKey}:outcome`;
1876
+ }
1877
+ function synchronousAssetSubmitAcceptanceKey(idempotencyKey) {
1878
+ return `${idempotencyKey}:accepted`;
1879
+ }
1880
+ function cachedSynchronousAssetSubmitFailure(outcome) {
1881
+ const retryAfterMs = positiveFiniteNumber(outcome.body['retry_after_ms']);
1882
+ return new hubNs.PublishRejectedError(typeof outcome.body['status'] === 'string'
1883
+ ? outcome.body['status']
1884
+ : String(outcome.body['error'] ?? 'rejected'), true, typeof outcome.body['reason'] === 'string' ? outcome.body['reason'] : undefined, retryAfterMs, false);
1885
+ }
1886
+ function isSynchronousAssetSubmitEnvelope(envelope) {
1887
+ return envelope.type === 'asset_submit'
1888
+ && envelope.idempotencyKey.startsWith(SYNC_ASSET_SUBMIT_PREFIX);
1889
+ }
1890
+ function isTerminalSynchronousPublishFailure(error) {
1891
+ return error instanceof hubNs.PublishRejectedError && error.terminal;
1892
+ }
1893
+ function isRetryableSynchronousPublishFailure(error) {
1894
+ if (error instanceof AuthError || errorName(error) === 'AuthError')
1895
+ return true;
1896
+ if (error instanceof HubUnreachableError || errorName(error) === 'HubUnreachableError')
1897
+ return true;
1898
+ if (error instanceof hubNs.PublishRejectedError) {
1899
+ return !error.terminal && (error.retryable === true || error.retryAfterMs !== undefined);
1900
+ }
1901
+ if (error instanceof HubClientError || errorName(error) === 'HubClientError') {
1902
+ const status = error instanceof HubClientError ? error.status : Number(asRecord(error)['status']);
1903
+ return status === 429 || (status >= 500 && status <= 599);
1904
+ }
1905
+ return false;
1906
+ }
1907
+ function synchronousPublishRetryAfterMs(error, failure) {
1908
+ const fromBody = positiveFiniteNumber(failure.body['retry_after_ms']);
1909
+ if (fromBody !== undefined)
1910
+ return Math.max(1_000, fromBody);
1911
+ if (error instanceof hubNs.PublishRejectedError && error.retryAfterMs !== undefined) {
1912
+ return Math.max(1_000, error.retryAfterMs);
1913
+ }
1914
+ if (error instanceof HubUnreachableError)
1915
+ return Math.max(1_000, error.retryAfterMs);
1916
+ if (error instanceof HubClientError && error.retryAfterMs !== undefined) {
1917
+ return Math.max(1_000, error.retryAfterMs);
1918
+ }
1919
+ return 60_000;
1920
+ }
1921
+ function submittedAssetIds(result, fallback) {
1922
+ const record = asRecord(result);
1923
+ for (const key of ['submittedAssetIds', 'assetIds']) {
1924
+ const value = record[key];
1925
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
1926
+ return value;
1927
+ }
1928
+ return fallback.map((asset) => asset.asset_id);
1929
+ }
1930
+ function mapSynchronousPublishFailure(error) {
1931
+ if (error instanceof hubNs.PublishRejectedError) {
1932
+ const status = publishRejectionStatus(error.status);
1933
+ if (status === 'cooldown') {
1934
+ return {
1935
+ statusCode: 429,
1936
+ body: {
1937
+ error: 'hub_rate_limited',
1938
+ ...(error.retryAfterMs !== undefined ? { retry_after_ms: error.retryAfterMs } : {}),
1939
+ },
1940
+ };
1941
+ }
1942
+ if (status === 'credit_shortage') {
1943
+ return { statusCode: 402, body: { error: 'hub_payment_required' } };
1944
+ }
1945
+ return {
1946
+ statusCode: error.terminal ? 422 : 503,
1947
+ body: {
1948
+ error: 'publish_rejected',
1949
+ status,
1950
+ terminal: error.terminal,
1951
+ reason: status === 'leak_blocked'
1952
+ ? 'sensitive data detected before publish'
1953
+ : 'Hub did not accept the publish',
1954
+ ...(error.retryAfterMs !== undefined ? { retry_after_ms: error.retryAfterMs } : {}),
1955
+ },
1956
+ };
1957
+ }
1958
+ if (error instanceof AuthError || errorName(error) === 'AuthError') {
1959
+ return { statusCode: 502, body: { error: 'hub_auth_failed' } };
1960
+ }
1961
+ if (error instanceof HubUnreachableError || errorName(error) === 'HubUnreachableError') {
1962
+ const retryAfterMs = error instanceof HubUnreachableError ? error.retryAfterMs : positiveFiniteNumber(asRecord(error)['retryAfterMs']);
1963
+ return {
1964
+ statusCode: 503,
1965
+ body: { error: 'hub_unreachable', ...(retryAfterMs !== undefined ? { retry_after_ms: retryAfterMs } : {}) },
1966
+ };
1967
+ }
1968
+ if (error instanceof HubClientError || errorName(error) === 'HubClientError') {
1969
+ const status = error instanceof HubClientError ? error.status : Number(asRecord(error)['status']);
1970
+ if (status === 429) {
1971
+ const retryAfterMs = error instanceof HubClientError ? error.retryAfterMs : positiveFiniteNumber(asRecord(error)['retryAfterMs']);
1972
+ return {
1973
+ statusCode: 429,
1974
+ body: { error: 'hub_rate_limited', ...(retryAfterMs !== undefined ? { retry_after_ms: retryAfterMs } : {}) },
1975
+ };
1976
+ }
1977
+ if (status === 402)
1978
+ return { statusCode: 402, body: { error: 'hub_payment_required' } };
1979
+ return { statusCode: status >= 500 ? 503 : 502, body: { error: 'hub_publish_failed' } };
1980
+ }
1981
+ return { statusCode: 502, body: { error: 'hub_publish_failed' } };
1982
+ }
1983
+ function errorName(value) {
1984
+ return typeof asRecord(value)['name'] === 'string' ? String(asRecord(value)['name']) : undefined;
1985
+ }
1986
+ function publishRejectionStatus(value) {
1987
+ return value === 'quarantine'
1988
+ || value === 'leak_blocked'
1989
+ || value === 'cooldown'
1990
+ || value === 'credit_shortage'
1991
+ ? value
1992
+ : 'rejected';
1993
+ }
951
1994
  function respondAgentDirectory(ctx, result) {
952
1995
  if (result.ok) {
953
1996
  ctx.json(200, result);