@evomap/evolver-proxy 2.0.0-beta.9 → 2.0.1
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.
- package/dist/bin/evolver-proxy.d.ts +26 -2
- package/dist/bin/evolver-proxy.js +284 -51
- package/dist/daemon/atpConsent.js +5 -2
- package/dist/daemon/collaborationFacade.js +23 -13
- package/dist/daemon/proxyDaemon.d.ts +52 -0
- package/dist/daemon/proxyDaemon.js +1067 -24
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/selectHub.js +5 -3
- package/dist/daemon/systemdNotifier.d.ts +46 -0
- package/dist/daemon/systemdNotifier.js +153 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +1 -0
- package/dist/lifecycle/deployGuard.js +1 -53
- package/dist/lifecycle/legacyNodeId.js +1 -178
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +1 -390
- package/dist/llm/bodyCapture.js +1 -293
- package/dist/llm/index.js +1 -3
- package/dist/llm/server.js +1 -359
- package/dist/llm/traceBackfill.js +1 -525
- package/dist/llm/traceConfig.js +1 -44
- package/dist/llm/traceControl.js +1 -85
- package/dist/llm/traceEnvelope.js +1 -286
- package/dist/llm/traceSink.js +1 -278
- package/dist/llm/traceUploadPayload.js +1 -27
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +1 -491
- package/dist/private/accountAssetCompatibility.d.ts +29 -0
- package/dist/private/accountAssetCompatibility.js +196 -0
- package/dist/private/adapterLoader.d.ts +21 -1
- package/dist/private/adapterLoader.js +242 -7
- package/dist/private/nodeCredentialStore.d.ts +23 -0
- package/dist/private/nodeCredentialStore.js +210 -0
- package/dist/router/cachePassthrough.js +1 -13
- package/dist/router/features.js +1 -52
- package/dist/router/index.js +1 -6
- package/dist/router/messagesRoute.js +1 -809
- package/dist/router/modelRouter.js +1 -66
- package/dist/router/providerRoutes.js +1 -1579
- package/dist/router/sseScan.js +1 -543
- package/dist/selfUpdate/bootstrap.d.ts +69 -0
- package/dist/selfUpdate/bootstrap.js +282 -0
- package/dist/selfUpdate/builtinKey.d.ts +4 -0
- package/dist/selfUpdate/builtinKey.js +16 -0
- package/dist/selfUpdate/executor.d.ts +1 -1
- package/dist/selfUpdate/executor.js +1 -1
- package/dist/selfUpdate/failureCodes.d.ts +4 -0
- package/dist/selfUpdate/failureCodes.js +7 -0
- package/dist/selfUpdate/migration.d.ts +93 -0
- package/dist/selfUpdate/migration.js +315 -0
- package/dist/selfUpdate/policy.d.ts +19 -2
- package/dist/selfUpdate/policy.js +82 -2
- package/dist/selfUpdate/releaseBinary.js +4 -1
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +1 -505
- 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
|
-
|
|
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) =>
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
610
|
-
|
|
611
|
-
.
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
|
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: {
|
|
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 =
|
|
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
|
-
|
|
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);
|