@evomap/evolver-proxy 2.0.0-beta.18 → 2.0.0-beta.19
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 +11 -0
- package/dist/bin/evolver-proxy.js +54 -10
- package/dist/daemon/collaborationFacade.js +23 -13
- package/dist/daemon/proxyDaemon.d.ts +50 -0
- package/dist/daemon/proxyDaemon.js +1026 -24
- package/dist/daemon/publishRecallVerifier.d.ts +114 -0
- package/dist/daemon/publishRecallVerifier.js +495 -0
- package/dist/daemon/systemdNotifier.d.ts +46 -0
- package/dist/daemon/systemdNotifier.js +153 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lifecycle/claimNudge.d.ts +20 -0
- package/dist/lifecycle/claimNudge.js +124 -0
- package/dist/lifecycle/manager.d.ts +4 -0
- package/dist/lifecycle/manager.js +15 -2
- package/dist/llm/upstream.d.ts +5 -1
- package/dist/llm/upstream.js +24 -1
- package/dist/sync/engine.d.ts +12 -0
- package/dist/sync/engine.js +255 -64
- package/package.json +6 -3
|
@@ -107,6 +107,7 @@ interface ProxyLoopDaemon {
|
|
|
107
107
|
nextDelay: (last: InboundResult) => number;
|
|
108
108
|
sleep?: (delayMs: number) => Promise<void>;
|
|
109
109
|
setWakeHandler?: (wake: (() => void) | undefined) => void;
|
|
110
|
+
setExpectedNextTick?: (delayMs: number | undefined) => void;
|
|
110
111
|
}
|
|
111
112
|
interface ProxyLoopLogger {
|
|
112
113
|
write: (chunk: string) => unknown;
|
|
@@ -121,6 +122,16 @@ export interface ProxyLoopOptions {
|
|
|
121
122
|
maxConsecutiveTickFailures?: number;
|
|
122
123
|
}
|
|
123
124
|
export declare function runProxyLoop(daemon: ProxyLoopDaemon, options?: ProxyLoopOptions): Promise<void>;
|
|
125
|
+
export declare function runManagedProxyLoop(options: {
|
|
126
|
+
daemon: ProxyLoopDaemon & StartupStoppableDaemon;
|
|
127
|
+
store: StartupClosableStore;
|
|
128
|
+
notifier: {
|
|
129
|
+
readyOrThrow(): Promise<void>;
|
|
130
|
+
stop(): void;
|
|
131
|
+
};
|
|
132
|
+
runLoop?: (daemon: ProxyLoopDaemon, options?: ProxyLoopOptions) => Promise<void>;
|
|
133
|
+
logger?: ProxyLoopLogger;
|
|
134
|
+
}): Promise<void>;
|
|
124
135
|
interface CreateProxyDaemonDepsOptions {
|
|
125
136
|
runtime: HubRuntime;
|
|
126
137
|
store: mailbox.MailboxStore;
|
|
@@ -13,10 +13,12 @@ import { traceCollectionEnabled } from '../llm/traceConfig.js';
|
|
|
13
13
|
import { connectPrivateProxyHub, resolvePrivateEnterpriseToken, resolvePrivateInvitationToken, resolvePrivateNodeSecret, } from '../private/adapterLoader.js';
|
|
14
14
|
import { PrivateNodeCredentialStore, PrivateNodeCredentialReadError, } from '../private/nodeCredentialStore.js';
|
|
15
15
|
import { createAtpOrderConsentGate } from '../daemon/atpConsent.js';
|
|
16
|
+
import { SystemdNotifier } from '../daemon/systemdNotifier.js';
|
|
16
17
|
import { resolveProxyStorePath } from './proxyStorePath.js';
|
|
17
18
|
import { publishProxySettings } from './proxySettings.js';
|
|
18
19
|
import { expandHomePath, loadEnvFileFromEnv } from './envFile.js';
|
|
19
20
|
import { readLegacyNodeId, resolveProxyNodeId } from '../lifecycle/legacyNodeId.js';
|
|
21
|
+
import { createClaimNudge, wrapHelloWithClaimNudge } from '../lifecycle/claimNudge.js';
|
|
20
22
|
import { getCurrentVersion } from '../selfUpdate/version.js';
|
|
21
23
|
import { resolveSelfUpdatePolicy } from '../selfUpdate/policy.js';
|
|
22
24
|
import { atomicReplaceExecutable, downloadGithubReleaseArtifact, resolveGithubReleaseManifest, resolveSelfUpdateTarget, } from '../selfUpdate/releaseBinary.js';
|
|
@@ -153,7 +155,16 @@ export async function runProxyMain(options = {}) {
|
|
|
153
155
|
proxySettingsState.url = `http://127.0.0.1:${port}`;
|
|
154
156
|
publishLocalProxySettings();
|
|
155
157
|
process.stdout.write(`[evolver-proxy] mode=${mode} hub=${hubUrl} ipc=127.0.0.1:${port} v=${evolverVersion} self-update=${selfUpdatePolicy}\n`);
|
|
156
|
-
|
|
158
|
+
const systemdNotifier = new SystemdNotifier({
|
|
159
|
+
env: process.env,
|
|
160
|
+
health: () => proxyDaemon.health(),
|
|
161
|
+
});
|
|
162
|
+
await runManagedProxyLoop({
|
|
163
|
+
daemon: proxyDaemon,
|
|
164
|
+
store: store,
|
|
165
|
+
notifier: systemdNotifier,
|
|
166
|
+
logger: process.stderr,
|
|
167
|
+
});
|
|
157
168
|
}
|
|
158
169
|
export async function recoverBoundDurableSelfUpdate(options) {
|
|
159
170
|
return recoverDurableSelfUpdate({
|
|
@@ -455,6 +466,7 @@ export async function runProxyLoop(daemon, options = {}) {
|
|
|
455
466
|
let consecutiveTickFailures = 0;
|
|
456
467
|
try {
|
|
457
468
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
469
|
+
daemon.setExpectedNextTick?.(undefined);
|
|
458
470
|
let delayMs = errorDelayMs;
|
|
459
471
|
let tickHealthy = false;
|
|
460
472
|
let exitForResolvedFailure;
|
|
@@ -494,7 +506,9 @@ export async function runProxyLoop(daemon, options = {}) {
|
|
|
494
506
|
break;
|
|
495
507
|
if (tickHealthy) {
|
|
496
508
|
// Healthy idle: wake-interruptible so new outbound/inbound work re-ticks promptly.
|
|
497
|
-
|
|
509
|
+
const healthyDelayMs = Math.max(minDelayMs, delayMs);
|
|
510
|
+
daemon.setExpectedNextTick?.(healthyDelayMs);
|
|
511
|
+
await sleepUntilDelayOrWake(healthyDelayMs, sleep, setWakeHandler);
|
|
498
512
|
}
|
|
499
513
|
else {
|
|
500
514
|
// Error / fatal-candidate backoff: NON-interruptible. Otherwise wakeRunner()
|
|
@@ -506,13 +520,30 @@ export async function runProxyLoop(daemon, options = {}) {
|
|
|
506
520
|
}
|
|
507
521
|
}
|
|
508
522
|
finally {
|
|
523
|
+
daemon.setExpectedNextTick?.(undefined);
|
|
509
524
|
if (!useDaemonSleep)
|
|
510
525
|
daemon.setWakeHandler?.(undefined);
|
|
511
526
|
}
|
|
512
527
|
}
|
|
528
|
+
export async function runManagedProxyLoop(options) {
|
|
529
|
+
try {
|
|
530
|
+
await options.notifier.readyOrThrow();
|
|
531
|
+
await (options.runLoop ?? runProxyLoop)(options.daemon, options.logger ? { logger: options.logger } : {});
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
try {
|
|
535
|
+
options.notifier.stop();
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
await closeStartupResources({ store: options.store, daemon: options.daemon });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
513
542
|
export function createProxyDaemonDeps(options) {
|
|
514
543
|
const selfUpdate = createSelfUpdateDeps(options.selfUpdatePolicy, options.evolverVersion, options.env ?? process.env, options.selfUpdateOverrides);
|
|
515
|
-
const
|
|
544
|
+
const env = options.env ?? process.env;
|
|
545
|
+
const traceBackfill = resolveTraceBackfillConfig(env);
|
|
546
|
+
const heartbeatIntervalMs = positiveIntegerEnv(env['HEARTBEAT_INTERVAL_MS']);
|
|
516
547
|
return {
|
|
517
548
|
hub: options.runtime.hub,
|
|
518
549
|
...(options.hubMode ? { hubMode: options.hubMode } : {}),
|
|
@@ -526,11 +557,19 @@ export function createProxyDaemonDeps(options) {
|
|
|
526
557
|
evolverVersion: options.evolverVersion,
|
|
527
558
|
hello: options.runtime.hello,
|
|
528
559
|
heartbeat: options.runtime.heartbeat,
|
|
560
|
+
...(heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs } : {}),
|
|
529
561
|
...(options.runtime.helloMode ? { helloMode: options.runtime.helloMode } : {}),
|
|
530
562
|
...(selfUpdate ? { selfUpdate } : {}),
|
|
531
563
|
...(traceBackfill ? { traceBackfill } : {}),
|
|
532
564
|
};
|
|
533
565
|
}
|
|
566
|
+
function positiveIntegerEnv(value) {
|
|
567
|
+
const trimmed = value?.trim();
|
|
568
|
+
if (!trimmed || !/^\d+$/.test(trimmed))
|
|
569
|
+
return undefined;
|
|
570
|
+
const parsed = Number(trimmed);
|
|
571
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
572
|
+
}
|
|
534
573
|
function resolveTraceBackfillConfig(env) {
|
|
535
574
|
if (!traceCollectionEnabled(env))
|
|
536
575
|
return undefined;
|
|
@@ -996,16 +1035,21 @@ export async function connectHubRuntime(deps) {
|
|
|
996
1035
|
clearDivergedPublicNodeSecret(deps.store, process.env);
|
|
997
1036
|
},
|
|
998
1037
|
});
|
|
1038
|
+
const hello = wrapHelloWithClaimNudge(async (opts) => {
|
|
1039
|
+
const result = await hub.hello(opts);
|
|
1040
|
+
if (result.nodeId)
|
|
1041
|
+
adoptVerifiedPublicNodeId(deps.store, selection, verifiedSender, result.nodeId);
|
|
1042
|
+
return result;
|
|
1043
|
+
}, createClaimNudge({
|
|
1044
|
+
store: deps.store,
|
|
1045
|
+
hubUrl: deps.hubUrl,
|
|
1046
|
+
env: deps.env ?? process.env,
|
|
1047
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
1048
|
+
}));
|
|
999
1049
|
return {
|
|
1000
1050
|
hub,
|
|
1001
1051
|
atp: new AtpHubClient({ baseUrl: deps.hubUrl, auth, fetchFn: globalFetchLike, senderId }),
|
|
1002
|
-
hello
|
|
1003
|
-
const result = await hub.hello(opts);
|
|
1004
|
-
if (result.nodeId) {
|
|
1005
|
-
adoptVerifiedPublicNodeId(deps.store, selection, verifiedSender, result.nodeId);
|
|
1006
|
-
}
|
|
1007
|
-
return result;
|
|
1008
|
-
},
|
|
1052
|
+
hello,
|
|
1009
1053
|
heartbeat: (opts) => hub.heartbeat(opts),
|
|
1010
1054
|
};
|
|
1011
1055
|
}
|
|
@@ -353,16 +353,19 @@ export class CollaborationFacade {
|
|
|
353
353
|
async mailboxPoll(ctx) {
|
|
354
354
|
const body = asRecord(await ctx.readJson());
|
|
355
355
|
const type = optionalString(body['type']);
|
|
356
|
-
const
|
|
356
|
+
const channel = optionalString(body['channel']);
|
|
357
|
+
const direction = optionalDirection(body['direction']) ?? 'inbound';
|
|
357
358
|
const limit = boundedMailboxPollLimit(body['limit']);
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
359
|
+
const runtimeNamespace = legacyMailboxRuntimeNamespace(channel, this.deps.runtimeNamespace ?? 'default');
|
|
360
|
+
const messages = runtimeNamespace === undefined
|
|
361
|
+
? []
|
|
362
|
+
: this.deps.store.list({
|
|
363
|
+
status: 'pending',
|
|
364
|
+
runtimeNamespace,
|
|
365
|
+
...(type ? { type } : {}),
|
|
366
|
+
direction,
|
|
367
|
+
limit,
|
|
368
|
+
}).map(v1Message);
|
|
366
369
|
ctx.json(200, { messages, count: messages.length });
|
|
367
370
|
return true;
|
|
368
371
|
}
|
|
@@ -671,18 +674,19 @@ function v1Message(message) {
|
|
|
671
674
|
return {
|
|
672
675
|
id: message.id,
|
|
673
676
|
message_id: message.id,
|
|
674
|
-
channel: 'evomap-hub',
|
|
677
|
+
channel: message.runtimeNamespace === 'default' ? 'evomap-hub' : message.runtimeNamespace,
|
|
675
678
|
direction: message.direction,
|
|
676
679
|
type: message.type === 'dm_outbound' || isDirectDialogMessage(message) ? 'dm' : message.type,
|
|
677
680
|
status: message.status === 'done' ? (message.direction === 'inbound' ? 'delivered' : 'synced') : message.status,
|
|
678
681
|
payload: message.payload,
|
|
679
|
-
priority:
|
|
680
|
-
ref_id: message.replyTo,
|
|
682
|
+
priority: message.priority,
|
|
683
|
+
ref_id: message.replyTo ?? message.correlationId,
|
|
681
684
|
created_at: message.createdAt,
|
|
682
685
|
synced_at: message.status === 'done' ? message.updatedAt : null,
|
|
683
686
|
expires_at: message.ttlAt,
|
|
684
687
|
retry_count: message.attempts,
|
|
685
|
-
|
|
688
|
+
next_retry_at: message.nextRetryAt,
|
|
689
|
+
error: message.lastError,
|
|
686
690
|
};
|
|
687
691
|
}
|
|
688
692
|
function writeOperationError(ctx, error) {
|
|
@@ -794,6 +798,12 @@ function optionalString(value) {
|
|
|
794
798
|
function optionalDirection(value) {
|
|
795
799
|
return value === 'inbound' || value === 'outbound' || value === 'local' ? value : undefined;
|
|
796
800
|
}
|
|
801
|
+
function legacyMailboxRuntimeNamespace(requestedChannel, runtimeNamespace) {
|
|
802
|
+
if (requestedChannel === undefined || requestedChannel === 'evomap-hub' || requestedChannel === runtimeNamespace) {
|
|
803
|
+
return runtimeNamespace;
|
|
804
|
+
}
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
797
807
|
function requiredValue(record, ...keys) {
|
|
798
808
|
for (const key of keys) {
|
|
799
809
|
const value = optionalString(record[key]);
|
|
@@ -3,6 +3,7 @@ import { SyncEngine, type OutboundResult, type InboundResult } from '../sync/eng
|
|
|
3
3
|
import { LifecycleManager, type HelloLifecycleMode, type HelloResult, type HeartbeatOptions, type HeartbeatResult, type HeartbeatTickResult } from '../lifecycle/manager.js';
|
|
4
4
|
import { type SelfUpdateDeps } from '../selfUpdate/executor.js';
|
|
5
5
|
import type { AtpOrderConsentGate } from './atpConsent.js';
|
|
6
|
+
import { type PublishRecallVerifierPort } from './publishRecallVerifier.js';
|
|
6
7
|
type HubCapability = hubNs.HubCapability;
|
|
7
8
|
type AssetStoreProvider = assetstore.AssetStoreProvider;
|
|
8
9
|
export declare const DEFAULT_IPC_PORT = 19820;
|
|
@@ -18,6 +19,16 @@ export interface ProxyDaemonDeps {
|
|
|
18
19
|
ipcHost?: string;
|
|
19
20
|
runtimeNamespace?: string;
|
|
20
21
|
now?: () => number;
|
|
22
|
+
/** Hub lifecycle heartbeat cadence. LifecycleManager applies its 30s safety floor and backoff policy. */
|
|
23
|
+
heartbeatIntervalMs?: number;
|
|
24
|
+
/** Remote asset-search cache TTL. Local asset-store reads are never cached. */
|
|
25
|
+
assetSearchCacheTtlMs?: number;
|
|
26
|
+
/** Maximum number of remote asset-search results cached by this daemon. */
|
|
27
|
+
assetSearchCacheMax?: number;
|
|
28
|
+
/** Time after cache expiry during which a rate-limited search may serve stale remote results. */
|
|
29
|
+
assetSearchStaleGraceMs?: number;
|
|
30
|
+
/** Maximum time the compatibility HTTP request waits before returning a durable pending receipt. */
|
|
31
|
+
assetSubmitResponseTimeoutMs?: number;
|
|
21
32
|
/** Random source for heartbeat force_update staggering. Defaults to Math.random; tests inject a deterministic value. */
|
|
22
33
|
random?: () => number;
|
|
23
34
|
/** 注入 adapter 的 /a2a/hello + heartbeat wire 调用(M6-6 提供; M6-4 测试用 fake). evolverVersion 随报供 hub 观测. */
|
|
@@ -60,6 +71,10 @@ export interface ProxyDaemonDeps {
|
|
|
60
71
|
};
|
|
61
72
|
/** V1 collaboration task operations are synchronous; tests may shorten the Hub timeout. */
|
|
62
73
|
collaborationOperationTimeoutMs?: number;
|
|
74
|
+
/** Optional test/composition seam. Production defaults to the restart-safe verifier backed by this daemon's store. */
|
|
75
|
+
publishRecallVerifier?: PublishRecallVerifierPort;
|
|
76
|
+
/** Optional deterministic sanitizer environment for composition tests. Production omits this to scan process.env. */
|
|
77
|
+
publishSanitizeEnv?: Record<string, string | undefined>;
|
|
63
78
|
}
|
|
64
79
|
export interface ProxyTickReport {
|
|
65
80
|
outbound: OutboundResult;
|
|
@@ -77,8 +92,12 @@ export interface ProxyTickError {
|
|
|
77
92
|
export interface ProxyHealth {
|
|
78
93
|
running: boolean;
|
|
79
94
|
ipcListening: boolean;
|
|
95
|
+
lifecycleArmed: boolean;
|
|
80
96
|
nodeId?: string;
|
|
81
97
|
lastWriteAt: number;
|
|
98
|
+
lastTickAt?: number;
|
|
99
|
+
nextTickDueAt?: number;
|
|
100
|
+
consecutiveFailures: number;
|
|
82
101
|
}
|
|
83
102
|
export interface AtpProxyClient {
|
|
84
103
|
placeOrder(opts: {
|
|
@@ -123,9 +142,21 @@ export declare class ProxyDaemon {
|
|
|
123
142
|
private readonly validator;
|
|
124
143
|
private readonly atp;
|
|
125
144
|
private readonly collaborationFacade;
|
|
145
|
+
private readonly publishRecallVerifier;
|
|
146
|
+
private readonly proxyHandler;
|
|
126
147
|
private ipc;
|
|
127
148
|
private readonly now;
|
|
128
149
|
private readonly random;
|
|
150
|
+
private readonly assetSearchCacheTtlMs;
|
|
151
|
+
private readonly assetSearchCacheMax;
|
|
152
|
+
private readonly assetSearchStaleGraceMs;
|
|
153
|
+
private readonly assetSubmitResponseTimeoutMs;
|
|
154
|
+
private readonly synchronousAssetSubmitScope;
|
|
155
|
+
private readonly shadowMode;
|
|
156
|
+
private readonly assetSearchCache;
|
|
157
|
+
private readonly assetSearchInflight;
|
|
158
|
+
private readonly synchronousAssetSubmitInflight;
|
|
159
|
+
private assetSearchCooldownUntil;
|
|
129
160
|
private nextHeartbeatAt;
|
|
130
161
|
private heartbeatFailures;
|
|
131
162
|
private heartbeatGeneration;
|
|
@@ -134,6 +165,10 @@ export declare class ProxyDaemon {
|
|
|
134
165
|
/** A poke that arrived between ticks (no sleep in flight) parks the wake here so it is not lost. */
|
|
135
166
|
private wakeRunnerPending;
|
|
136
167
|
private started;
|
|
168
|
+
private lifecycleArmed;
|
|
169
|
+
private lastTickAt;
|
|
170
|
+
private nextTickDueAt;
|
|
171
|
+
private consecutiveTickFailures;
|
|
137
172
|
private storeClosed;
|
|
138
173
|
private forceUpdateTriggerInFlight;
|
|
139
174
|
private forceUpdateLastTriggeredAt;
|
|
@@ -159,6 +194,7 @@ export declare class ProxyDaemon {
|
|
|
159
194
|
/** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
|
|
160
195
|
nextDelay(last: InboundResult): number;
|
|
161
196
|
setWakeHandler(wake: (() => void) | undefined): void;
|
|
197
|
+
setExpectedNextTick(delayMs: number | undefined): void;
|
|
162
198
|
notifyNewOutbound(): void;
|
|
163
199
|
/**
|
|
164
200
|
* Expedite the next heartbeat: clear the failure backoff, mark the heartbeat due now, and wake an
|
|
@@ -191,6 +227,20 @@ export declare class ProxyDaemon {
|
|
|
191
227
|
private stateNumber;
|
|
192
228
|
private handleProxyRoute;
|
|
193
229
|
private searchAssets;
|
|
230
|
+
private searchRemoteAssets;
|
|
231
|
+
private cacheRemoteAssetSearch;
|
|
232
|
+
private publishAssetSubmitSynchronously;
|
|
233
|
+
private createSynchronousAssetSubmitEnvelope;
|
|
234
|
+
private currentHubMode;
|
|
235
|
+
private handleHubModeBoundOutbound;
|
|
236
|
+
private publishSynchronousBundle;
|
|
237
|
+
private waitForSynchronousAssetSubmit;
|
|
238
|
+
private executeSynchronousAssetSubmit;
|
|
239
|
+
private handleSynchronousProxyOutbound;
|
|
240
|
+
private readSynchronousAssetSubmitOutcome;
|
|
241
|
+
private cacheSynchronousAssetSubmitSuccess;
|
|
242
|
+
private cacheSynchronousAssetSubmitTerminal;
|
|
243
|
+
private writeSynchronousAssetSubmitOutcome;
|
|
194
244
|
private handleAtpRoute;
|
|
195
245
|
private writeAtpJson;
|
|
196
246
|
}
|