@evomap/evolver-proxy 2.0.0-beta.4 → 2.0.0-beta.6

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.
@@ -39,7 +39,7 @@ interface RollbackPendingStartupOptions {
39
39
  }
40
40
  export declare function rollbackPendingStartup(options: RollbackPendingStartupOptions): Promise<SelfUpdateRecoveryResult>;
41
41
  export declare function startupRollbackExitCode(rollback: SelfUpdateRecoveryResult): 78;
42
- export declare function proxyUsage(): string;
42
+ export declare function proxyUsage(command?: string): string;
43
43
  export interface RunProxyCliOptions {
44
44
  argv?: readonly string[];
45
45
  env?: NodeJS.ProcessEnv;
@@ -52,6 +52,7 @@ export interface RunProxyCliOptions {
52
52
  }
53
53
  export interface ProxyCliPathOptions {
54
54
  home?: string;
55
+ evomapHome?: string;
55
56
  store?: string;
56
57
  settings?: string;
57
58
  envFile?: string;
@@ -235,18 +235,19 @@ export function startupRollbackExitCode(rollback) {
235
235
  }
236
236
  return 78;
237
237
  }
238
- export function proxyUsage() {
238
+ export function proxyUsage(command = 'evolver-proxy') {
239
239
  return [
240
- 'usage: evolver-proxy [options]',
240
+ `Usage: ${command} [options]`,
241
241
  '',
242
242
  'Starts the local Evolver proxy daemon.',
243
243
  '',
244
244
  'Options (CLI overrides environment variables):',
245
- ' --home <dir> Root for assets, store, settings, and traces',
246
- ' --store <path> Mailbox store path (EVOLVER_PROXY_STORE)',
247
- ' --settings <path> Proxy settings file (EVOLVER_PROXY_SETTINGS_FILE)',
248
- ' --env-file <path> Environment file (EVOLVER_ENV_FILE)',
249
- ' -h, --help Show this help',
245
+ ' --home <dir> Root for assets, store, settings, and traces',
246
+ ' --evomap-home <dir> Identity home for node_id/node_secret (EVOMAP_HOME); defaults to --home',
247
+ ' --store <path> Mailbox store path (EVOLVER_PROXY_STORE)',
248
+ ' --settings <path> Proxy settings file (EVOLVER_PROXY_SETTINGS_FILE)',
249
+ ' --env-file <path> Environment file (EVOLVER_ENV_FILE)',
250
+ ' -h, --help Show this help',
250
251
  '',
251
252
  'Required for public mode:',
252
253
  ' EVOMAP_NODE_SECRET or A2A_NODE_SECRET',
@@ -267,6 +268,7 @@ export function proxyUsage() {
267
268
  }
268
269
  const PROXY_PATH_FLAGS = new Map([
269
270
  ['--home', 'home'],
271
+ ['--evomap-home', 'evomapHome'],
270
272
  ['--store', 'store'],
271
273
  ['--settings', 'settings'],
272
274
  ['--env-file', 'envFile'],
@@ -309,6 +311,11 @@ export function prepareProxyCliEnvironment(argv, env) {
309
311
  env['EVOLVER_PROXY_SETTINGS_FILE'] = join(options.home, 'settings.json');
310
312
  env['EVOLVER_LLM_TRACE_DIR'] = join(options.home, 'proxy', 'traces');
311
313
  }
314
+ // Identity/state split for embedders whose node identity lives outside the state root (evox agentDir keeps
315
+ // node_id/node_secret under <agentDir>/evomap while evolver state lives under <agentDir>/evolver, #555 T2).
316
+ // Applied AFTER --home so it overrides the single-root EVOMAP_HOME derivation; state paths stay on --home.
317
+ if (options.evomapHome)
318
+ env['EVOMAP_HOME'] = options.evomapHome;
312
319
  if (options.store)
313
320
  env['EVOLVER_PROXY_STORE'] = options.store;
314
321
  if (options.settings)
@@ -346,7 +353,7 @@ export async function runProxyCli(options = {}) {
346
353
  try {
347
354
  const cliOptions = parseProxyCliPathOptions(argv);
348
355
  if (cliOptions.help) {
349
- process.stdout.write(proxyUsage());
356
+ process.stdout.write(proxyUsage(argv[0] === 'proxy' ? 'evolver proxy' : 'evolver-proxy'));
350
357
  return 0;
351
358
  }
352
359
  const prepared = prepareProxyCliEnvironment(argv, env);
@@ -706,13 +713,29 @@ function readTrimmedFile(path) {
706
713
  return undefined;
707
714
  }
708
715
  }
716
+ // Identity-home probe order (#555 T2): EVOMAP_HOME is THE identity home and outranks the state root
717
+ // (EVOLVER_HOME) — under the evox agentDir split (`--home <agentDir>/evolver --evomap-home <agentDir>/evomap`)
718
+ // node files live only under the evomap dir, and the old single-home read (EVOLVER_HOME-first) would miss
719
+ // them and fall back to the machine-global ~/.evomap node. Probing is a fall-through union, so single-home
720
+ // setups (only EVOLVER_HOME, or neither) resolve exactly as before.
721
+ function identityHomeCandidates(env = process.env) {
722
+ const candidates = [
723
+ env['EVOMAP_HOME'],
724
+ env['EVOMAP_DIR'],
725
+ env['EVOLVER_HOME'],
726
+ join(env['HOME'] || homedir(), '.evomap'),
727
+ ];
728
+ return [...new Set(candidates.map((value) => value?.trim()).filter((value) => Boolean(value)))];
729
+ }
709
730
  function readLegacyNodeSecret(env = process.env) {
710
- const home = evomapHome(env);
711
- const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
712
- if (!nodeSecret || !isNodeSecret(nodeSecret))
713
- return undefined;
714
- const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
715
- return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
731
+ for (const home of identityHomeCandidates(env)) {
732
+ const nodeSecret = readTrimmedFile(join(home, 'node_secret'));
733
+ if (!nodeSecret || !isNodeSecret(nodeSecret))
734
+ continue;
735
+ const nodeSecretVersion = parseNodeSecretVersion(readTrimmedFile(join(home, 'node_secret_version')));
736
+ return { nodeSecret, ...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}) };
737
+ }
738
+ return undefined;
716
739
  }
717
740
  // Durable copies of the legacy node_secret, cleared on hub-signalled divergence.
718
741
  // Store keys mirror cli LOCAL_SECRET_STATE_KEYS (index.ts:82); on-disk files mirror
@@ -728,13 +751,17 @@ const LEGACY_SECRET_FILES = ['node_secret', 'node_secret_version'];
728
751
  export function clearDivergedPublicNodeSecret(store, env = process.env) {
729
752
  for (const key of LOCAL_SECRET_STATE_KEYS)
730
753
  store.setState(key, '');
731
- const home = evomapHome(env);
732
- for (const file of LEGACY_SECRET_FILES) {
733
- try {
734
- rmSync(join(home, file), { force: true });
735
- }
736
- catch (err) {
737
- process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
754
+ // Wipe every identity-home candidate, not just the resolved state home: under the identity/state split
755
+ // (EVOMAP_HOME EVOLVER_HOME) the diverged files live in the evomap dir, and clearing only one home would
756
+ // leave them to resurrect the diverged secret on the next start (same union rationale as reset-local-secret).
757
+ for (const home of identityHomeCandidates(env)) {
758
+ for (const file of LEGACY_SECRET_FILES) {
759
+ try {
760
+ rmSync(join(home, file), { force: true });
761
+ }
762
+ catch (err) {
763
+ process.stderr.write(`[evolver-proxy] failed to unlink diverged ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
764
+ }
738
765
  }
739
766
  }
740
767
  }
@@ -151,6 +151,7 @@ export declare class ProxyDaemon {
151
151
  private recordTickError;
152
152
  /** 启动: 锁 + IPC 监听 + 初次 hello. 返回 IPC 端口. */
153
153
  start(): Promise<number>;
154
+ private listenIpc;
154
155
  /** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
155
156
  tick(): Promise<ProxyTickReport>;
156
157
  /** 下一轮建议延时: inbound 背压/idle 与 outbound pending cadence 取更快者. */
@@ -1,5 +1,5 @@
1
1
  import { dirname, join } from 'node:path';
2
- import { mailbox, hub as hubNs, shadow as shadow_, assetstore } from '@evomap/evolver-core';
2
+ import { mailbox, hub as hubNs, shadow as shadow_, assetstore, util } from '@evomap/evolver-core';
3
3
  import { SyncEngine, SYNC_INTERVALS } from '../sync/engine.js';
4
4
  import { LifecycleManager } from '../lifecycle/manager.js';
5
5
  import { executeForceUpdate } from '../selfUpdate/executor.js';
@@ -11,6 +11,7 @@ export const DEFAULT_IPC_PORT = 19820;
11
11
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
12
12
  const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
13
13
  const MAX_HEARTBEAT_TICK_ERROR_LENGTH = 1_000;
14
+ const MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS = 5;
14
15
  /**
15
16
  * ProxyDaemon(M6-4) 装配层: 把 core(MailboxStore/Dispatcher/MailboxDaemon/IpcServer) +
16
17
  * HubBindings(M6-1) + SyncEngine(M6-2) + LifecycleManager(M6-3) 拼成系统级 proxy.
@@ -164,7 +165,7 @@ export class ProxyDaemon {
164
165
  ...(this.deps.onIpcAuthFailure ? { onAuthFailure: this.deps.onIpcAuthFailure } : {}),
165
166
  extraRoutes: [(ctx) => this.handleProxyRoute(ctx)],
166
167
  });
167
- const port = await this.ipc.listen(this.deps.ipcPort ?? DEFAULT_IPC_PORT);
168
+ const port = await this.listenIpc(this.ipc);
168
169
  try {
169
170
  this.deps.onIpcListen?.(port);
170
171
  }
@@ -188,6 +189,22 @@ export class ProxyDaemon {
188
189
  throw err;
189
190
  }
190
191
  }
192
+ async listenIpc(ipc) {
193
+ const requestedPort = this.deps.ipcPort ?? DEFAULT_IPC_PORT;
194
+ if (requestedPort !== 0)
195
+ return ipc.listen(requestedPort);
196
+ for (let attempt = 0; attempt < MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS; attempt += 1) {
197
+ const assignedPort = await ipc.listen(0);
198
+ if (!util.isFetchForbiddenPort(assignedPort))
199
+ return assignedPort;
200
+ // The outer start() cleanup owns the final listener when retries are exhausted.
201
+ if (attempt === MAX_EPHEMERAL_IPC_LISTEN_ATTEMPTS - 1) {
202
+ throw new Error('proxy_ipc_safe_port_unavailable');
203
+ }
204
+ await ipc.close();
205
+ }
206
+ throw new Error('proxy_ipc_safe_port_unavailable');
207
+ }
191
208
  /** 单轮: core pump/TTL/wake + proxy 出站 + hub 入站 + 到点心跳. */
192
209
  async tick() {
193
210
  const errors = [];
@@ -19,12 +19,13 @@ export interface LegacyNodeIdCandidateOptions {
19
19
  * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
20
20
  * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
21
21
  * never hides an id that lives under one of the home overrides. Deduped.
22
- * 1. `<evomapHome>/node_id` — the override-aware home file. `resolveEvomapHome()`
23
- * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
24
- * EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
25
- * home relocated via EVOLVER_HOME reads the file the v1 writer put there
26
- * the lesson of v1 #120, which routed the reader through the same helper as
27
- * the writer.
22
+ * 1. `<identity home>/node_id` — the override-aware home files. `identityHomeCandidates()`
23
+ * probes EVOMAP_HOME first (THE identity home, #555 T2 — the evox agentDir split keeps
24
+ * node files under `<agentDir>/evomap` while EVOLVER_HOME points at the state root) and
25
+ * then EVOLVER_HOME (matching v1 `getEvomapDir`), after dropping blank/relative
26
+ * overrides. Probing both keeps the v1 #120 lesson the reader walks every dir a
27
+ * writer may have used — while the order makes a split identity dir win over the
28
+ * state root when both hold files.
28
29
  * 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
29
30
  * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
30
31
  * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
@@ -17,11 +17,22 @@ function cleanAbsolutePath(value) {
17
17
  return undefined;
18
18
  return isAbsolute(trimmed) ? trimmed : undefined;
19
19
  }
20
- function resolveEvomapHome(opts, fallbackHomeDir) {
21
- return cleanAbsolutePath(opts.evomapHomeDir)
22
- ?? cleanAbsolutePath(process.env['EVOLVER_HOME'])
23
- ?? cleanAbsolutePath(process.env['EVOMAP_HOME'])
24
- ?? (fallbackHomeDir === undefined ? undefined : join(fallbackHomeDir, '.evomap'));
20
+ // Identity homes in probe order (#555 T2): the explicit test/caller override wins outright; otherwise
21
+ // EVOMAP_HOME (THE identity home) outranks EVOLVER_HOME (the state root) so the evox agentDir split
22
+ // (`--evomap-home <agentDir>/evomap` + `--home <agentDir>/evolver`) reads node files from the evomap dir
23
+ // instead of missing them and falling through to the machine-global ~/.evomap node. Both homes stay in the
24
+ // candidate union, so single-home setups resolve exactly as before.
25
+ function identityHomeCandidates(opts, fallbackHomeDir) {
26
+ const fromOpts = cleanAbsolutePath(opts.evomapHomeDir);
27
+ if (fromOpts !== undefined)
28
+ return [fromOpts];
29
+ const homes = [
30
+ cleanAbsolutePath(process.env['EVOMAP_HOME']),
31
+ cleanAbsolutePath(process.env['EVOLVER_HOME']),
32
+ ].filter((value) => value !== undefined);
33
+ if (homes.length > 0)
34
+ return homes;
35
+ return fallbackHomeDir === undefined ? [] : [join(fallbackHomeDir, '.evomap')];
25
36
  }
26
37
  function packageNameAt(dir) {
27
38
  const pkgPath = join(dir, 'package.json');
@@ -66,12 +77,13 @@ function installRootNodeIdCandidates(moduleDir) {
66
77
  * and let the hub mint a duplicate. Probed FIRST and ADDITIVELY (the
67
78
  * EVOLVER_HOME/EVOMAP_HOME candidates below are kept), so setting EVOMAP_DIR
68
79
  * never hides an id that lives under one of the home overrides. Deduped.
69
- * 1. `<evomapHome>/node_id` — the override-aware home file. `resolveEvomapHome()`
70
- * honours EVOLVER_HOME (matching v1 `getEvomapDir`) and ADDITIONALLY
71
- * EVOMAP_HOME after dropping blank/relative overrides. It wins first so a
72
- * home relocated via EVOLVER_HOME reads the file the v1 writer put there
73
- * the lesson of v1 #120, which routed the reader through the same helper as
74
- * the writer.
80
+ * 1. `<identity home>/node_id` — the override-aware home files. `identityHomeCandidates()`
81
+ * probes EVOMAP_HOME first (THE identity home, #555 T2 — the evox agentDir split keeps
82
+ * node files under `<agentDir>/evomap` while EVOLVER_HOME points at the state root) and
83
+ * then EVOLVER_HOME (matching v1 `getEvomapDir`), after dropping blank/relative
84
+ * overrides. Probing both keeps the v1 #120 lesson the reader walks every dir a
85
+ * writer may have used — while the order makes a split identity dir win over the
86
+ * state root when both hold files.
75
87
  * 2. `~/.evomap/node_id` — the UNCONDITIONAL v1 location. v1's writer pivots on
76
88
  * `getEvomapDir` = `EVOLVER_HOME || ~/.evomap` and ignores EVOMAP_HOME
77
89
  * entirely, so unless EVOLVER_HOME was set a v1 file always physically lands
@@ -97,12 +109,12 @@ function installRootNodeIdCandidates(moduleDir) {
97
109
  export function legacyNodeIdCandidates(opts = {}) {
98
110
  const home = cleanAbsolutePath(opts.homeDir) ?? cleanAbsolutePath(homedir());
99
111
  const moduleDir = opts.moduleDir ?? _moduleDir;
100
- const evomapHome = resolveEvomapHome(opts, home);
112
+ const evomapHomes = identityHomeCandidates(opts, home);
101
113
  const evomapDir = cleanAbsolutePath(opts.evomapDir) ?? cleanAbsolutePath(process.env['EVOMAP_DIR']);
102
114
  return [
103
115
  ...new Set([
104
116
  ...(evomapDir === undefined ? [] : [join(evomapDir, 'node_id')]),
105
- ...(evomapHome === undefined ? [] : [join(evomapHome, 'node_id')]),
117
+ ...evomapHomes.map((dir) => join(dir, 'node_id')),
106
118
  ...(home === undefined ? [] : [join(home, '.evomap', 'node_id')]),
107
119
  ...installRootNodeIdCandidates(moduleDir),
108
120
  ]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-proxy",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "系统级 mailbox/hub 同步 daemon (Node)",
@@ -26,8 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@aws-sdk/client-bedrock-runtime": "^3.1053.0",
29
- "@evomap/evolver-adapter-public": "2.0.0-beta.4",
30
- "@evomap/evolver-core": "2.0.0-beta.4"
29
+ "@evomap/evolver-adapter-public": "2.0.0-beta.6",
30
+ "@evomap/evolver-core": "2.0.0-beta.6"
31
31
  },
32
32
  "repository": {
33
33
  "type": "git",