@hirey/hi-mcp-server 0.1.10 → 0.1.11

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/README.md CHANGED
@@ -91,9 +91,10 @@ npm install -g @hirey/hi-mcp-server
91
91
  1. 用宿主把 `hi-mcp-server` 作为 `stdio` MCP server 挂上
92
92
  2. 优先执行 `hi_agent_install`,而不是让用户手工走 `register -> connect -> activate`
93
93
  3. `hi_agent_install` 会自动完成 register / activate、delivery capability 声明、默认订阅,以及在 OpenClaw 场景下的 receiver 配置
94
- 4. `agent_id` 可以省略;省略时由 gateway 正式生成 canonical `ag_...`,不需要用户手写
95
- 5. 完成后执行 `hi_agent_doctor`
96
- 6. 如果要 clean reinstall,先执行 `hi_agent_reset`
94
+ 4. 当 receiver 的物料配置(例如 hooks token、adapter URL、transport、default reply route)发生变化时,`hi_agent_install` 会先停止当前 profile 记录的 receiver,再写入新配置并按需要重新启动;如果配置没变,则保留现有 runtime cursor,不重置消费进度
95
+ 5. `agent_id` 可以省略;省略时由 gateway 正式生成 canonical `ag_...`,不需要用户手写
96
+ 6. 完成后执行 `hi_agent_doctor`
97
+ 7. 如果要 clean reinstall,先执行 `hi_agent_reset`;`hi_agent_reset` 现在会等待当前 profile 记录的 receiver 真实退出,再删除本地 state / receiver config,避免立刻重装时旧进程把旧配置写回去
97
98
 
98
99
  如果宿主不能直接执行裸命令 `hi-agent-receiver`,而是需要 `node /path/to/dist/server.js`、`npx ...` 或其它显式 argv 前缀,`hi_agent_install` 现在也支持把 receiver 启动入口写成 `receiver_command_argv` 数组;`hi-mcp-server` 会在这组 argv 后自动追加 `run --config <path>`,不再把整段命令误当成单个可执行文件。
99
100
 
@@ -0,0 +1,4 @@
1
+ export declare function receiverConfigMaterialSnapshot(raw: unknown): Record<string, unknown> | null;
2
+ export declare function receiverConfigMaterialEquals(left: unknown, right: unknown): boolean;
3
+ export declare function applyReceiverRuntimeSnapshot(nextConfig: Record<string, unknown>, existingConfig: unknown): Record<string, unknown>;
4
+ //# sourceMappingURL=receiver-config-material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receiver-config-material.d.ts","sourceRoot":"","sources":["../src/receiver-config-material.ts"],"names":[],"mappings":"AAcA,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQ3F;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAEnF;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,cAAc,EAAE,OAAO,GACtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB"}
@@ -0,0 +1,47 @@
1
+ function isPlainObject(value) {
2
+ return !!value && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ function canonicalizeJson(value) {
5
+ if (Array.isArray(value))
6
+ return value.map((entry) => canonicalizeJson(entry));
7
+ if (!isPlainObject(value))
8
+ return value;
9
+ const out = {};
10
+ for (const key of Object.keys(value).sort()) {
11
+ out[key] = canonicalizeJson(value[key]);
12
+ }
13
+ return out;
14
+ }
15
+ export function receiverConfigMaterialSnapshot(raw) {
16
+ if (!isPlainObject(raw))
17
+ return null;
18
+ const snapshot = JSON.parse(JSON.stringify(raw));
19
+ delete snapshot.runtime;
20
+ delete snapshot._generated_at;
21
+ delete snapshot._generated_by;
22
+ delete snapshot._receiver_config_path;
23
+ return canonicalizeJson(snapshot);
24
+ }
25
+ export function receiverConfigMaterialEquals(left, right) {
26
+ return JSON.stringify(receiverConfigMaterialSnapshot(left)) === JSON.stringify(receiverConfigMaterialSnapshot(right));
27
+ }
28
+ export function applyReceiverRuntimeSnapshot(nextConfig, existingConfig) {
29
+ if (!isPlainObject(existingConfig) || !isPlainObject(existingConfig.runtime))
30
+ return nextConfig;
31
+ const runtime = existingConfig.runtime;
32
+ return {
33
+ ...nextConfig,
34
+ // 安装修复时保留已消费 cursor,避免仅因重写物料配置就把 receiver 从头回放。
35
+ runtime: {
36
+ last_consumed_stream_seq: Number.isFinite(Number(runtime.last_consumed_stream_seq))
37
+ ? Math.max(0, Number(runtime.last_consumed_stream_seq))
38
+ : 0,
39
+ last_claim_lease_id: typeof runtime.last_claim_lease_id === 'string'
40
+ ? runtime.last_claim_lease_id
41
+ : null,
42
+ updated_at: typeof runtime.updated_at === 'string'
43
+ ? runtime.updated_at
44
+ : null,
45
+ },
46
+ };
47
+ }
package/dist/server.js CHANGED
@@ -14,8 +14,11 @@ import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGat
14
14
  import { readState, resolveCanonicalOpenClawStateDir, resolveDefaultStateDir, resolveLegacyStateFiles, resolveStateFile, updateState, normalizeStateProfile, } from './state.js';
15
15
  import { looksLikeOpenClawSessionKey, validateOpenClawSessionKey, } from './openclaw-session-key.js';
16
16
  import { buildInstallReceiverCommandArgv, } from './receiver-command.js';
17
+ import { applyReceiverRuntimeSnapshot, receiverConfigMaterialEquals, } from './receiver-config-material.js';
17
18
  import { resolveInstallDefaultReplyDeliveryContext, } from './defaultReplyRoute.js';
18
19
  const CAPABILITY_CACHE_TTL_MS = 30_000;
20
+ const RECEIVER_STOP_TIMEOUT_MS = 3_000;
21
+ const RECEIVER_STOP_POLL_MS = 100;
19
22
  const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
20
23
  const config = {
21
24
  host: normalizeText(process.env.HI_MCP_HOST) || '127.0.0.1',
@@ -724,6 +727,58 @@ function isProcessAlive(pid) {
724
727
  return false;
725
728
  }
726
729
  }
730
+ async function waitForProcessExit(pid, timeoutMs = RECEIVER_STOP_TIMEOUT_MS) {
731
+ const deadline = Date.now() + timeoutMs;
732
+ while (Date.now() < deadline) {
733
+ if (!isProcessAlive(pid))
734
+ return true;
735
+ await sleep(RECEIVER_STOP_POLL_MS);
736
+ }
737
+ return !isProcessAlive(pid);
738
+ }
739
+ async function stopTrackedReceiverProcess(pidRaw) {
740
+ const pid = Number(pidRaw || 0);
741
+ if (!Number.isInteger(pid) || pid <= 0) {
742
+ return {
743
+ attempted: false,
744
+ pid: null,
745
+ signal: null,
746
+ exited: true,
747
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
748
+ };
749
+ }
750
+ if (!isProcessAlive(pid)) {
751
+ return {
752
+ attempted: true,
753
+ pid,
754
+ signal: null,
755
+ exited: true,
756
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
757
+ };
758
+ }
759
+ try {
760
+ process.kill(pid, 'SIGTERM');
761
+ }
762
+ catch (error) {
763
+ if (error?.code === 'ESRCH') {
764
+ return {
765
+ attempted: true,
766
+ pid,
767
+ signal: 'SIGTERM',
768
+ exited: true,
769
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
770
+ };
771
+ }
772
+ throw error;
773
+ }
774
+ return {
775
+ attempted: true,
776
+ pid,
777
+ signal: 'SIGTERM',
778
+ exited: await waitForProcessExit(pid),
779
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
780
+ };
781
+ }
727
782
  async function startDetachedReceiver(args) {
728
783
  const [command, ...prefixArgs] = args.receiverCommandArgv;
729
784
  if (!command)
@@ -1242,7 +1297,8 @@ async function handleInstall(args) {
1242
1297
  return fail('missing_host_adapter_url');
1243
1298
  }
1244
1299
  const receiverConfigPath = resolveReceiverConfigPath(await loadPersistedState());
1245
- const receiverConfig = buildReceiverConfig({
1300
+ let existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
1301
+ const desiredReceiverConfig = buildReceiverConfig({
1246
1302
  receiverConfigPath,
1247
1303
  hostKind,
1248
1304
  receiverTransport,
@@ -1252,16 +1308,37 @@ async function handleInstall(args) {
1252
1308
  openresponsesModel: normalizeText(args.openresponses_model) || undefined,
1253
1309
  defaultReplyRoute,
1254
1310
  });
1255
- await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
1311
+ const receiverConfigChanged = !receiverConfigMaterialEquals(existingReceiverConfig, desiredReceiverConfig);
1312
+ let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1313
+ let receiverStop = null;
1314
+ if (receiverConfigChanged && isProcessAlive(receiverPid)) {
1315
+ // 先停 tracked receiver,避免旧进程继续拿旧 token 投递,并把旧物料配置再写回磁盘。
1316
+ receiverStop = await stopTrackedReceiverProcess(receiverPid);
1317
+ if (!normalizeBooleanFlag(receiverStop?.exited)) {
1318
+ return fail('receiver_stop_timeout', {
1319
+ receiver_config_path: receiverConfigPath,
1320
+ receiver_pid: receiverPid,
1321
+ receiver_stop: receiverStop,
1322
+ });
1323
+ }
1324
+ receiverPid = null;
1325
+ // 停掉 tracked receiver 后再读一次磁盘,尽量保留它退出前最后一次写下的 runtime cursor。
1326
+ existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
1327
+ }
1328
+ if (receiverConfigChanged) {
1329
+ const receiverConfig = applyReceiverRuntimeSnapshot(desiredReceiverConfig, existingReceiverConfig);
1330
+ await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
1331
+ }
1256
1332
  state = await persistState((current) => ({
1257
1333
  ...current,
1258
1334
  runtime: buildInstallRuntimeState(current.runtime, {
1259
1335
  host_kind: hostKind,
1260
1336
  receiver_config_path: receiverConfigPath,
1337
+ receiver_pid: isProcessAlive(receiverPid) ? receiverPid : null,
1261
1338
  receiver_last_error: null,
1262
1339
  }),
1263
1340
  }));
1264
- let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1341
+ receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1265
1342
  if (receiverShouldStart && !isProcessAlive(receiverPid)) {
1266
1343
  try {
1267
1344
  receiverPid = await startDetachedReceiver({
@@ -1297,12 +1374,14 @@ async function handleInstall(args) {
1297
1374
  }
1298
1375
  receiverPayload = {
1299
1376
  config_path: receiverConfigPath,
1377
+ config_changed: receiverConfigChanged,
1300
1378
  started: receiverShouldStart,
1301
1379
  receiver_pid: receiverPid || null,
1302
1380
  receiver_command_argv: receiverCommandArgv,
1303
1381
  transport: receiverTransport,
1304
1382
  host_adapter_kind: hostAdapterKind,
1305
1383
  host_adapter_url: hostAdapterUrl,
1384
+ ...(receiverStop ? { receiver_stop: receiverStop } : {}),
1306
1385
  };
1307
1386
  }
1308
1387
  if (receiverShouldStart) {
@@ -1335,22 +1414,21 @@ async function handleReset(args) {
1335
1414
  const removeReceiverConfig = args.remove_receiver_config !== false;
1336
1415
  const clearState = args.clear_state !== false;
1337
1416
  let stopResult = null;
1338
- if (stopReceiver && isProcessAlive(installState.receiver_pid)) {
1417
+ if (stopReceiver && Number.isInteger(installState.receiver_pid) && Number(installState.receiver_pid) > 0) {
1339
1418
  try {
1340
- process.kill(Number(installState.receiver_pid), 'SIGTERM');
1341
- stopResult = {
1342
- attempted: true,
1343
- pid: installState.receiver_pid,
1344
- signalled: true,
1345
- };
1419
+ stopResult = await stopTrackedReceiverProcess(installState.receiver_pid);
1346
1420
  }
1347
1421
  catch (error) {
1348
- stopResult = {
1349
- attempted: true,
1422
+ return fail('receiver_stop_failed', {
1350
1423
  pid: installState.receiver_pid,
1351
- signalled: false,
1352
1424
  error: String(error?.message || error || 'receiver_stop_failed'),
1353
- };
1425
+ });
1426
+ }
1427
+ if (!normalizeBooleanFlag(stopResult?.exited)) {
1428
+ return fail('receiver_stop_timeout', {
1429
+ pid: installState.receiver_pid,
1430
+ receiver_stop: stopResult,
1431
+ });
1354
1432
  }
1355
1433
  }
1356
1434
  const receiverConfigPath = installState.receiver_config_path || resolveReceiverConfigPath(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirey/hi-mcp-server",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/server.js",