@hermespilot/link 0.6.0 → 0.6.2

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.
@@ -1376,6 +1376,97 @@ var REASONING_EFFORTS = [
1376
1376
  "high",
1377
1377
  "xhigh"
1378
1378
  ];
1379
+ var EMPTY_AUTH_BACKED_PROVIDER_STATE = {
1380
+ providers: /* @__PURE__ */ new Set(),
1381
+ providerBaseUrls: /* @__PURE__ */ new Map(),
1382
+ externalProviders: /* @__PURE__ */ new Set()
1383
+ };
1384
+ var AUTH_BACKED_MODEL_PROVIDERS = [
1385
+ {
1386
+ provider: "openai-codex",
1387
+ providerName: "OpenAI Codex",
1388
+ baseUrl: "https://chatgpt.com/backend-api/codex",
1389
+ apiMode: "codex_responses",
1390
+ authType: "oauth_external",
1391
+ modelIds: [
1392
+ "gpt-5.5",
1393
+ "gpt-5.4-mini",
1394
+ "gpt-5.4",
1395
+ "gpt-5.3-codex",
1396
+ "gpt-5.3-codex-spark",
1397
+ "gpt-5.2-codex",
1398
+ "gpt-5.1-codex-max",
1399
+ "gpt-5.1-codex-mini"
1400
+ ]
1401
+ },
1402
+ {
1403
+ provider: "nous",
1404
+ providerName: "Nous Research",
1405
+ baseUrl: "https://inference-api.nousresearch.com/v1",
1406
+ apiMode: "chat_completions",
1407
+ authType: "oauth_device_code",
1408
+ modelIds: [
1409
+ "anthropic/claude-opus-4.7",
1410
+ "anthropic/claude-sonnet-4.6",
1411
+ "openai/gpt-5.5",
1412
+ "openai/gpt-5.4-mini",
1413
+ "openai/gpt-5.3-codex",
1414
+ "google/gemini-3-pro-preview",
1415
+ "qwen/qwen3.6-plus",
1416
+ "minimax/minimax-m2.7"
1417
+ ]
1418
+ },
1419
+ {
1420
+ provider: "qwen-oauth",
1421
+ providerName: "Qwen OAuth (Portal)",
1422
+ baseUrl: "https://portal.qwen.ai/v1",
1423
+ apiMode: "chat_completions",
1424
+ authType: "oauth_external",
1425
+ modelIds: [
1426
+ "qwen3.6-plus",
1427
+ "qwen3.5-plus",
1428
+ "qwen3-coder-plus",
1429
+ "qwen3-coder-next"
1430
+ ]
1431
+ },
1432
+ {
1433
+ provider: "google-gemini-cli",
1434
+ providerName: "Google Gemini (OAuth)",
1435
+ baseUrl: "cloudcode-pa://google",
1436
+ apiMode: "chat_completions",
1437
+ authType: "oauth_external",
1438
+ modelIds: [
1439
+ "gemini-3.1-pro-preview",
1440
+ "gemini-3-pro-preview",
1441
+ "gemini-3-flash-preview"
1442
+ ]
1443
+ },
1444
+ {
1445
+ provider: "minimax-oauth",
1446
+ providerName: "MiniMax (OAuth)",
1447
+ baseUrl: "https://api.minimax.io/anthropic",
1448
+ apiMode: "anthropic_messages",
1449
+ authType: "oauth_external",
1450
+ modelIds: ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]
1451
+ },
1452
+ {
1453
+ provider: "copilot",
1454
+ providerName: "GitHub Copilot",
1455
+ baseUrl: "https://api.githubcopilot.com",
1456
+ apiMode: "chat_completions",
1457
+ authType: "copilot",
1458
+ modelIds: [
1459
+ "gpt-5.4",
1460
+ "gpt-5.4-mini",
1461
+ "gpt-5.3-codex",
1462
+ "claude-sonnet-4.6",
1463
+ "gemini-3-pro-preview"
1464
+ ]
1465
+ }
1466
+ ];
1467
+ var AUTH_BACKED_MODEL_PROVIDER_BY_KEY = new Map(
1468
+ AUTH_BACKED_MODEL_PROVIDERS.map((provider) => [provider.provider, provider])
1469
+ );
1379
1470
  var PROFILE_PERMISSION_TOOLSETS = [
1380
1471
  {
1381
1472
  key: "web",
@@ -1632,11 +1723,13 @@ async function listHermesModelConfigs(profileName = "default", configPath = reso
1632
1723
  const env = await readHermesEnvFile(profileName);
1633
1724
  const defaultModel = readModelConfig(config.model).model ?? null;
1634
1725
  const defaultReasoningEffort = readProfileReasoningEffort(config);
1726
+ const authBackedProviders = await readHermesAuthBackedProviderState(profileName);
1635
1727
  const models = readManagedModelConfigs(
1636
1728
  config,
1637
1729
  env,
1638
1730
  defaultModel,
1639
- defaultReasoningEffort
1731
+ defaultReasoningEffort,
1732
+ authBackedProviders
1640
1733
  );
1641
1734
  const compressionModelId = readAuxiliaryCompressionModelId(config);
1642
1735
  return {
@@ -1683,7 +1776,11 @@ async function listHermesModelConfigCatalog(input) {
1683
1776
  existing.isDefault = existing.isDefault || model.isDefault;
1684
1777
  if (existing.credentialState !== "configured" && model.credentialState === "configured") {
1685
1778
  existing.credentialState = "configured";
1779
+ existing.credentialSource = model.credentialSource;
1686
1780
  }
1781
+ existing.authType = existing.authType ?? model.authType;
1782
+ existing.editable = existing.editable && model.editable;
1783
+ existing.isReadOnly = existing.isReadOnly || model.isReadOnly;
1687
1784
  continue;
1688
1785
  }
1689
1786
  items.set(key, {
@@ -1695,6 +1792,10 @@ async function listHermesModelConfigCatalog(input) {
1695
1792
  ...model.contextLength ? { contextLength: model.contextLength } : {},
1696
1793
  ...model.keyEnv ? { keyEnv: model.keyEnv } : {},
1697
1794
  credentialState: model.credentialState,
1795
+ credentialSource: model.credentialSource,
1796
+ ...model.authType ? { authType: model.authType } : {},
1797
+ editable: model.editable,
1798
+ isReadOnly: model.isReadOnly,
1698
1799
  isDefault: model.isDefault,
1699
1800
  ...model.reasoningEffort ? { reasoningEffort: model.reasoningEffort } : {},
1700
1801
  reasoningSupport: model.reasoningSupport,
@@ -1922,15 +2023,23 @@ async function deleteHermesModelConfig(modelId, profileName = "default", configP
1922
2023
  }
1923
2024
  const { document, config, existingRaw } = await readHermesConfigDocument(configPath);
1924
2025
  const env = await readHermesEnvFile(profileName);
2026
+ const authBackedProviders = await readHermesAuthBackedProviderState(profileName);
1925
2027
  const existingModels = readManagedModelConfigs(
1926
2028
  config,
1927
2029
  env,
1928
2030
  readModelConfig(config.model).model ?? null,
1929
- readProfileReasoningEffort(config)
2031
+ readProfileReasoningEffort(config),
2032
+ authBackedProviders
1930
2033
  );
1931
- if (!existingModels.some((model) => model.id === id)) {
2034
+ const deletingModel = findManagedModelById(existingModels, id);
2035
+ if (!deletingModel) {
1932
2036
  throw new Error(`model "${id}" is not configured`);
1933
2037
  }
2038
+ if (deletingModel.isReadOnly) {
2039
+ throw new Error(
2040
+ `model "${id}" is managed by Hermes auth and has no Profile config to delete`
2041
+ );
2042
+ }
1934
2043
  if (existingModels.length <= 1) {
1935
2044
  throw new Error(
1936
2045
  "\u81F3\u5C11\u9700\u8981\u4FDD\u7559\u4E00\u4E2A\u6A21\u578B\uFF0C\u907F\u514D Hermes Agent \u6CA1\u6709\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\u3002"
@@ -1946,7 +2055,8 @@ async function deleteHermesModelConfig(modelId, profileName = "default", configP
1946
2055
  config,
1947
2056
  env,
1948
2057
  null,
1949
- readProfileReasoningEffort(config)
2058
+ readProfileReasoningEffort(config),
2059
+ authBackedProviders
1950
2060
  )[0];
1951
2061
  if (nextDefault) {
1952
2062
  writeDefaultModelConfig(modelConfig, {
@@ -1989,19 +2099,33 @@ async function deleteHermesModelConfig(modelId, profileName = "default", configP
1989
2099
  async function saveHermesModelDefaults(input, profileName = "default", configPath = resolveHermesConfigPath(profileName)) {
1990
2100
  const taskModelId = input.taskModelId?.trim();
1991
2101
  const compressionModelId = input.compressionModelId?.trim();
1992
- if (!taskModelId && !compressionModelId) {
1993
- throw new Error("taskModelId or compressionModelId is required");
2102
+ const reasoningEffort = input.reasoningEffort ? normalizeReasoningEffort(input.reasoningEffort) : void 0;
2103
+ if (input.reasoningEffort && !reasoningEffort) {
2104
+ throw new Error(
2105
+ "reasoningEffort must be none, minimal, low, medium, high or xhigh"
2106
+ );
2107
+ }
2108
+ if (!taskModelId && !compressionModelId && !reasoningEffort) {
2109
+ throw new Error(
2110
+ "taskModelId, compressionModelId or reasoningEffort is required"
2111
+ );
1994
2112
  }
1995
2113
  const { document, config, existingRaw } = await readHermesConfigDocument(configPath);
1996
2114
  const env = await readHermesEnvFile(profileName);
2115
+ const authBackedProviders = await readHermesAuthBackedProviderState(profileName);
1997
2116
  if (taskModelId) {
1998
2117
  const models = readManagedModelConfigs(
1999
2118
  config,
2000
2119
  env,
2001
2120
  readModelConfig(config.model).model ?? null,
2002
- readProfileReasoningEffort(config)
2121
+ readProfileReasoningEffort(config),
2122
+ authBackedProviders
2003
2123
  );
2004
- const selected = findManagedModelById(models, taskModelId);
2124
+ const selected = findManagedModel(models, {
2125
+ id: taskModelId,
2126
+ provider: input.taskModelProvider,
2127
+ baseUrl: input.taskModelBaseUrl
2128
+ });
2005
2129
  if (!selected) {
2006
2130
  throw new Error(`model "${taskModelId}" is not configured`);
2007
2131
  }
@@ -2027,14 +2151,22 @@ async function saveHermesModelDefaults(input, profileName = "default", configPat
2027
2151
  writeProfileReasoningEffort(config, selected.reasoningEffort);
2028
2152
  }
2029
2153
  }
2154
+ if (reasoningEffort) {
2155
+ writeProfileReasoningEffort(config, reasoningEffort);
2156
+ }
2030
2157
  if (compressionModelId) {
2031
2158
  const models = readManagedModelConfigs(
2032
2159
  config,
2033
2160
  env,
2034
2161
  readModelConfig(config.model).model ?? null,
2035
- readProfileReasoningEffort(config)
2162
+ readProfileReasoningEffort(config),
2163
+ authBackedProviders
2036
2164
  );
2037
- const selected = findManagedModelById(models, compressionModelId);
2165
+ const selected = findManagedModel(models, {
2166
+ id: compressionModelId,
2167
+ provider: input.compressionModelProvider,
2168
+ baseUrl: input.compressionModelBaseUrl
2169
+ });
2038
2170
  if (!selected) {
2039
2171
  throw new Error(`model "${compressionModelId}" is not configured`);
2040
2172
  }
@@ -2501,7 +2633,7 @@ async function writeHermesConfigDocument(input) {
2501
2633
  );
2502
2634
  return backupPath;
2503
2635
  }
2504
- function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffort) {
2636
+ function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffort, authBackedProviders = EMPTY_AUTH_BACKED_PROVIDER_STATE) {
2505
2637
  const models = [];
2506
2638
  const seen = /* @__PURE__ */ new Set();
2507
2639
  const seenEndpoint = /* @__PURE__ */ new Map();
@@ -2511,11 +2643,17 @@ function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffo
2511
2643
  const entry = toRecord(rawEntry);
2512
2644
  const providerName = readString2(entry.name) ?? readString2(entry.provider_name) ?? readString2(entry.provider_key) ?? "Custom Provider";
2513
2645
  const provider = readString2(entry.provider_key) ?? readString2(entry.provider) ?? "custom";
2514
- const baseUrl = readString2(entry.base_url) ?? readString2(entry.url) ?? readString2(entry.api) ?? "";
2646
+ const authDefinition = AUTH_BACKED_MODEL_PROVIDER_BY_KEY.get(provider);
2647
+ const baseUrl = readString2(entry.base_url) ?? readString2(entry.url) ?? readString2(entry.api) ?? authDefinition?.baseUrl ?? "";
2515
2648
  const apiMode = inferApiMode(provider, baseUrl, readString2(entry.api_mode));
2516
2649
  const contextLength = readPositiveInteger(entry.context_length);
2517
2650
  const keyEnv = readString2(entry.key_env) ?? parseEnvReference(readString2(entry.api_key));
2518
- const credentialState = readCredentialState(entry, env);
2651
+ const credentialMetadata = readEntryCredentialMetadata(
2652
+ entry,
2653
+ env,
2654
+ provider,
2655
+ authBackedProviders
2656
+ );
2519
2657
  for (const id of readEntryModelIds(entry)) {
2520
2658
  const key = modelConfigKey(provider, baseUrl, id);
2521
2659
  if (seen.has(key)) {
@@ -2540,8 +2678,8 @@ function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffo
2540
2678
  apiMode,
2541
2679
  ...modelContextLength ? { contextLength: modelContextLength } : {},
2542
2680
  ...keyEnv ? { keyEnv } : {},
2543
- credentialConfigured: credentialState === "configured",
2544
- credentialState,
2681
+ credentialConfigured: credentialMetadata.credentialState === "configured",
2682
+ ...credentialMetadata,
2545
2683
  isDefault: id === defaultModel,
2546
2684
  ...reasoningEffort ? { reasoningEffort } : {},
2547
2685
  ...reasoningMetadata
@@ -2551,7 +2689,8 @@ function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffo
2551
2689
  }
2552
2690
  if (defaultModel) {
2553
2691
  const provider = modelConfig.provider ?? "default";
2554
- const baseUrl = modelConfig.baseUrl ?? "";
2692
+ const authDefinition = AUTH_BACKED_MODEL_PROVIDER_BY_KEY.get(provider);
2693
+ const baseUrl = modelConfig.baseUrl ?? authDefinition?.baseUrl ?? "";
2555
2694
  const endpointMatchIndex = seenEndpoint.get(
2556
2695
  modelEndpointKey(baseUrl, defaultModel)
2557
2696
  );
@@ -2565,39 +2704,263 @@ function readManagedModelConfigs(config, env, defaultModel, defaultReasoningEffo
2565
2704
  isDefault: true,
2566
2705
  reasoningEffort: existing.reasoningEffort ?? defaultReasoningEffort ?? void 0
2567
2706
  };
2568
- return models.sort(
2569
- (left, right) => Number(right.isDefault) - Number(left.isDefault)
2570
- );
2707
+ } else {
2708
+ const key = modelConfigKey(provider, baseUrl, defaultModel);
2709
+ if (!seen.has(key)) {
2710
+ const credentialMetadata = readModelCredentialMetadata(
2711
+ modelConfig,
2712
+ env,
2713
+ provider,
2714
+ authBackedProviders
2715
+ );
2716
+ const reasoningEffort = modelConfig.reasoningEffort ?? defaultReasoningEffort;
2717
+ const apiMode = inferApiMode(provider, baseUrl, modelConfig.apiMode);
2718
+ models.unshift({
2719
+ id: defaultModel,
2720
+ provider,
2721
+ providerName: authDefinition?.providerName ?? provider,
2722
+ source: "model_default",
2723
+ baseUrl,
2724
+ apiMode,
2725
+ ...modelConfig.contextLength ? { contextLength: modelConfig.contextLength } : {},
2726
+ ...modelConfig.keyEnv ? { keyEnv: modelConfig.keyEnv } : {},
2727
+ credentialConfigured: credentialMetadata.credentialState === "configured",
2728
+ ...credentialMetadata,
2729
+ isDefault: true,
2730
+ ...reasoningEffort ? { reasoningEffort } : {},
2731
+ ...modelReasoningMetadata({
2732
+ provider,
2733
+ baseUrl,
2734
+ modelId: defaultModel,
2735
+ apiMode
2736
+ })
2737
+ });
2738
+ seen.add(key);
2739
+ seenEndpoint.set(modelEndpointKey(baseUrl, defaultModel), 0);
2740
+ }
2571
2741
  }
2572
- const key = modelConfigKey(provider, baseUrl, defaultModel);
2573
- if (!seen.has(key)) {
2574
- const credentialState = readModelCredentialState(modelConfig, env);
2575
- const reasoningEffort = modelConfig.reasoningEffort ?? defaultReasoningEffort;
2576
- models.unshift({
2577
- id: defaultModel,
2578
- provider,
2579
- providerName: provider,
2580
- source: "model_default",
2742
+ }
2743
+ appendAuthBackedModelConfigs({
2744
+ models,
2745
+ seen,
2746
+ seenEndpoint,
2747
+ defaultModel,
2748
+ defaultProvider: modelConfig.provider ?? null,
2749
+ defaultReasoningEffort,
2750
+ authBackedProviders
2751
+ });
2752
+ return models.sort(
2753
+ (left, right) => Number(right.isDefault) - Number(left.isDefault)
2754
+ );
2755
+ }
2756
+ function appendAuthBackedModelConfigs(input) {
2757
+ for (const definition of AUTH_BACKED_MODEL_PROVIDERS) {
2758
+ if (!input.authBackedProviders.providers.has(definition.provider)) {
2759
+ continue;
2760
+ }
2761
+ const baseUrl = input.authBackedProviders.providerBaseUrls.get(definition.provider) ?? definition.baseUrl;
2762
+ const credentialSource = input.authBackedProviders.externalProviders.has(
2763
+ definition.provider
2764
+ ) ? "external_auth" : "auth_store";
2765
+ for (const id of definition.modelIds) {
2766
+ const key = modelConfigKey(definition.provider, baseUrl, id);
2767
+ if (input.seen.has(key)) {
2768
+ continue;
2769
+ }
2770
+ input.seen.add(key);
2771
+ const apiMode = inferApiMode(
2772
+ definition.provider,
2581
2773
  baseUrl,
2582
- apiMode: inferApiMode(provider, baseUrl, modelConfig.apiMode),
2583
- ...modelConfig.contextLength ? { contextLength: modelConfig.contextLength } : {},
2584
- ...modelConfig.keyEnv ? { keyEnv: modelConfig.keyEnv } : {},
2585
- credentialConfigured: credentialState === "configured",
2586
- credentialState,
2587
- isDefault: true,
2588
- ...reasoningEffort ? { reasoningEffort } : {},
2774
+ definition.apiMode
2775
+ );
2776
+ const isDefault = id === input.defaultModel && definition.provider === input.defaultProvider;
2777
+ input.models.push({
2778
+ id,
2779
+ provider: definition.provider,
2780
+ providerName: definition.providerName,
2781
+ source: "auth_store",
2782
+ baseUrl,
2783
+ apiMode,
2784
+ credentialConfigured: true,
2785
+ credentialState: "configured",
2786
+ credentialSource,
2787
+ authType: definition.authType,
2788
+ editable: false,
2789
+ isReadOnly: true,
2790
+ isDefault,
2791
+ ...isDefault && input.defaultReasoningEffort ? { reasoningEffort: input.defaultReasoningEffort } : {},
2589
2792
  ...modelReasoningMetadata({
2590
- provider,
2793
+ provider: definition.provider,
2591
2794
  baseUrl,
2592
- modelId: defaultModel,
2593
- apiMode: inferApiMode(provider, baseUrl, modelConfig.apiMode)
2795
+ modelId: id,
2796
+ apiMode
2594
2797
  })
2595
2798
  });
2799
+ input.seenEndpoint.set(
2800
+ modelEndpointKey(baseUrl, id),
2801
+ input.models.length - 1
2802
+ );
2596
2803
  }
2597
2804
  }
2598
- return models.sort(
2599
- (left, right) => Number(right.isDefault) - Number(left.isDefault)
2805
+ }
2806
+ function readEntryCredentialMetadata(entry, env, provider, authBackedProviders) {
2807
+ const apiKey = readString2(entry.api_key);
2808
+ const keyEnv = readString2(entry.key_env) ?? parseEnvReference(apiKey);
2809
+ return readCredentialMetadata({
2810
+ provider,
2811
+ inlineApiKey: apiKey && !parseEnvReference(apiKey) ? apiKey : void 0,
2812
+ keyEnv,
2813
+ env,
2814
+ authBackedProviders
2815
+ });
2816
+ }
2817
+ function readModelCredentialMetadata(model, env, provider, authBackedProviders) {
2818
+ return readCredentialMetadata({
2819
+ provider,
2820
+ inlineApiKey: model.apiKey && !model.keyEnv ? model.apiKey : void 0,
2821
+ keyEnv: model.keyEnv,
2822
+ env,
2823
+ authBackedProviders
2824
+ });
2825
+ }
2826
+ function readCredentialMetadata(input) {
2827
+ const definition = AUTH_BACKED_MODEL_PROVIDER_BY_KEY.get(input.provider);
2828
+ if (definition) {
2829
+ const configured = input.authBackedProviders.providers.has(input.provider);
2830
+ return {
2831
+ credentialState: configured ? "configured" : "missing",
2832
+ credentialSource: input.authBackedProviders.externalProviders.has(
2833
+ input.provider
2834
+ ) ? "external_auth" : "auth_store",
2835
+ authType: definition.authType,
2836
+ editable: false,
2837
+ isReadOnly: true
2838
+ };
2839
+ }
2840
+ if (input.inlineApiKey?.trim()) {
2841
+ return {
2842
+ credentialState: "configured",
2843
+ credentialSource: "api_key",
2844
+ editable: true,
2845
+ isReadOnly: false
2846
+ };
2847
+ }
2848
+ if (input.keyEnv) {
2849
+ return {
2850
+ credentialState: input.env[input.keyEnv]?.trim() ? "configured" : "missing",
2851
+ credentialSource: "env",
2852
+ editable: true,
2853
+ isReadOnly: false
2854
+ };
2855
+ }
2856
+ return {
2857
+ credentialState: "unknown",
2858
+ credentialSource: "unknown",
2859
+ editable: true,
2860
+ isReadOnly: false
2861
+ };
2862
+ }
2863
+ async function readHermesAuthBackedProviderState(profileName) {
2864
+ const providers = /* @__PURE__ */ new Set();
2865
+ const providerBaseUrls = /* @__PURE__ */ new Map();
2866
+ const externalProviders = /* @__PURE__ */ new Set();
2867
+ const profileDir = resolveHermesProfileDir(profileName);
2868
+ const authStore = await readJsonRecordFile(path3.join(profileDir, "auth.json"));
2869
+ const storeProviders = toRecord(authStore.providers);
2870
+ for (const [provider, rawState] of Object.entries(storeProviders)) {
2871
+ if (!AUTH_BACKED_MODEL_PROVIDER_BY_KEY.has(provider)) {
2872
+ continue;
2873
+ }
2874
+ const state = toRecord(rawState);
2875
+ if (!authRecordHasUsableCredential(state)) {
2876
+ continue;
2877
+ }
2878
+ providers.add(provider);
2879
+ const baseUrl = readAuthRecordBaseUrl(state);
2880
+ if (baseUrl) {
2881
+ providerBaseUrls.set(provider, baseUrl);
2882
+ }
2883
+ }
2884
+ const credentialPool = toRecord(authStore.credential_pool);
2885
+ for (const [provider, rawEntries] of Object.entries(credentialPool)) {
2886
+ if (!AUTH_BACKED_MODEL_PROVIDER_BY_KEY.has(provider)) {
2887
+ continue;
2888
+ }
2889
+ if (!Array.isArray(rawEntries)) {
2890
+ continue;
2891
+ }
2892
+ const entry = rawEntries.map(toRecord).find((candidate) => authRecordHasUsableCredential(candidate));
2893
+ if (!entry) {
2894
+ continue;
2895
+ }
2896
+ providers.add(provider);
2897
+ const baseUrl = readAuthRecordBaseUrl(entry);
2898
+ if (baseUrl) {
2899
+ providerBaseUrls.set(provider, baseUrl);
2900
+ }
2901
+ }
2902
+ if (await hasQwenCliAuth()) {
2903
+ providers.add("qwen-oauth");
2904
+ externalProviders.add("qwen-oauth");
2905
+ }
2906
+ if (await hasGoogleGeminiCliAuth(profileDir)) {
2907
+ providers.add("google-gemini-cli");
2908
+ externalProviders.add("google-gemini-cli");
2909
+ }
2910
+ return { providers, providerBaseUrls, externalProviders };
2911
+ }
2912
+ async function readJsonRecordFile(filePath) {
2913
+ const raw = await readFile2(filePath, "utf8").catch((error) => {
2914
+ if (isNodeError3(error, "ENOENT")) {
2915
+ return null;
2916
+ }
2917
+ throw error;
2918
+ });
2919
+ if (!raw) {
2920
+ return {};
2921
+ }
2922
+ try {
2923
+ return toRecord(JSON.parse(raw));
2924
+ } catch {
2925
+ return {};
2926
+ }
2927
+ }
2928
+ function authRecordHasUsableCredential(record) {
2929
+ const credentialKeys = [
2930
+ "access",
2931
+ "access_token",
2932
+ "api_key",
2933
+ "runtime_api_key",
2934
+ "agent_key",
2935
+ "refresh",
2936
+ "refresh_token"
2937
+ ];
2938
+ if (credentialKeys.some((key) => readString2(record[key])?.trim())) {
2939
+ return true;
2940
+ }
2941
+ const tokens = toRecord(record.tokens);
2942
+ return credentialKeys.some((key) => readString2(tokens[key])?.trim());
2943
+ }
2944
+ function readAuthRecordBaseUrl(record) {
2945
+ const direct = readString2(record.inference_base_url) ?? readString2(record.base_url) ?? readString2(record.portal_base_url);
2946
+ if (direct?.trim()) {
2947
+ return direct.trim();
2948
+ }
2949
+ const tokens = toRecord(record.tokens);
2950
+ const nested = readString2(tokens.inference_base_url) ?? readString2(tokens.base_url) ?? readString2(tokens.portal_base_url);
2951
+ return nested?.trim() || void 0;
2952
+ }
2953
+ async function hasQwenCliAuth() {
2954
+ const qwenAuth = await readJsonRecordFile(
2955
+ path3.join(os.homedir(), ".qwen", "oauth_creds.json")
2956
+ );
2957
+ return authRecordHasUsableCredential(qwenAuth);
2958
+ }
2959
+ async function hasGoogleGeminiCliAuth(profileDir) {
2960
+ const googleAuth = await readJsonRecordFile(
2961
+ path3.join(profileDir, "auth", "google_oauth.json")
2600
2962
  );
2963
+ return authRecordHasUsableCredential(googleAuth);
2601
2964
  }
2602
2965
  function readAuxiliaryCompressionModelId(config) {
2603
2966
  const auxiliary = toRecord(config.auxiliary);
@@ -2633,6 +2996,22 @@ function resolveCompressionModel(config, models) {
2633
2996
  function findManagedModelById(models, id) {
2634
2997
  return models.find((model) => model.id === id);
2635
2998
  }
2999
+ function findManagedModel(models, input) {
3000
+ const provider = input.provider?.trim();
3001
+ const baseUrl = input.baseUrl?.trim();
3002
+ return models.find((model) => {
3003
+ if (model.id !== input.id) {
3004
+ return false;
3005
+ }
3006
+ if (provider && model.provider !== provider) {
3007
+ return false;
3008
+ }
3009
+ if (baseUrl !== void 0 && normalizeBaseUrl(model.baseUrl) !== normalizeBaseUrl(baseUrl)) {
3010
+ return false;
3011
+ }
3012
+ return true;
3013
+ });
3014
+ }
2636
3015
  function writeAuxiliaryCompressionModelConfig(config, model, env) {
2637
3016
  const auxiliary = ensureRecord(config, "auxiliary");
2638
3017
  const compression = ensureRecord(auxiliary, "compression");
@@ -2969,11 +3348,15 @@ async function readHermesModelConfigForImport(input) {
2969
3348
  resolveHermesConfigPath(input.profileName)
2970
3349
  );
2971
3350
  const env = await readHermesEnvFile(input.profileName);
3351
+ const authBackedProviders = await readHermesAuthBackedProviderState(
3352
+ input.profileName
3353
+ );
2972
3354
  const models = readManagedModelConfigs(
2973
3355
  config,
2974
3356
  env,
2975
3357
  readModelConfig(config.model).model ?? null,
2976
- readProfileReasoningEffort(config)
3358
+ readProfileReasoningEffort(config),
3359
+ authBackedProviders
2977
3360
  );
2978
3361
  const model = models.find((candidate) => {
2979
3362
  if (candidate.id !== input.modelId) {
@@ -3038,26 +3421,6 @@ function compareCatalogItems(left, right) {
3038
3421
  }
3039
3422
  return left.id.localeCompare(right.id);
3040
3423
  }
3041
- function readCredentialState(entry, env) {
3042
- const apiKey = readString2(entry.api_key);
3043
- const keyEnv = readString2(entry.key_env) ?? parseEnvReference(apiKey);
3044
- if (apiKey && !parseEnvReference(apiKey)) {
3045
- return "configured";
3046
- }
3047
- if (!keyEnv) {
3048
- return "unknown";
3049
- }
3050
- return env[keyEnv]?.trim() ? "configured" : "missing";
3051
- }
3052
- function readModelCredentialState(model, env) {
3053
- if (model.apiKey && !model.keyEnv) {
3054
- return "configured";
3055
- }
3056
- if (!model.keyEnv) {
3057
- return "unknown";
3058
- }
3059
- return env[model.keyEnv]?.trim() ? "configured" : "missing";
3060
- }
3061
3424
  function modelConfigKey(provider, baseUrl, modelId) {
3062
3425
  return [provider, normalizeBaseUrl(baseUrl), modelId].join("\n").toLowerCase();
3063
3426
  }
@@ -4425,7 +4788,7 @@ async function listCronOutputFiles(profileName, jobId) {
4425
4788
  mtimeMs: fileStat.mtimeMs
4426
4789
  });
4427
4790
  }
4428
- return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path28, mtime }) => ({ path: path28, mtime }));
4791
+ return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path29, mtime }) => ({ path: path29, mtime }));
4429
4792
  }
4430
4793
  async function readCronOutput(outputPath) {
4431
4794
  const content = await readFile3(outputPath, "utf8");
@@ -4493,7 +4856,7 @@ import { setTimeout as delay } from "timers/promises";
4493
4856
  import { promisify as promisify2 } from "util";
4494
4857
 
4495
4858
  // src/runtime/logger.ts
4496
- import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
4859
+ import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, truncate } from "fs/promises";
4497
4860
  import os3 from "os";
4498
4861
  import path6 from "path";
4499
4862
 
@@ -4502,7 +4865,7 @@ import os2 from "os";
4502
4865
  import path5 from "path";
4503
4866
 
4504
4867
  // src/constants.ts
4505
- var LINK_VERSION = "0.6.0";
4868
+ var LINK_VERSION = "0.6.2";
4506
4869
  var LINK_COMMAND = "hermeslink";
4507
4870
  var LINK_DEFAULT_PORT = 52379;
4508
4871
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -4719,6 +5082,27 @@ function readRecentGatewayLogEntries(options = {}) {
4719
5082
  filePaths: options.filePaths ?? getGatewayLogFiles(paths)
4720
5083
  });
4721
5084
  }
5085
+ async function flushLogFiles(options) {
5086
+ const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
5087
+ const truncated = [];
5088
+ const removed = [];
5089
+ const filePaths = Array.from(new Set(options.filePaths.map((filePath) => path6.resolve(filePath))));
5090
+ for (const filePath of filePaths) {
5091
+ if (await fileExists(filePath)) {
5092
+ await truncate(filePath, 0);
5093
+ truncated.push(filePath);
5094
+ }
5095
+ for (let index = 1; index <= maxFiles; index += 1) {
5096
+ const rotated = rotatedLogFile(filePath, index);
5097
+ if (!await fileExists(rotated)) {
5098
+ continue;
5099
+ }
5100
+ await rm2(rotated, { force: true });
5101
+ removed.push(rotated);
5102
+ }
5103
+ }
5104
+ return { truncated, removed };
5105
+ }
4722
5106
  function clampLimit(value) {
4723
5107
  if (typeof value !== "number" || !Number.isFinite(value)) {
4724
5108
  return DEFAULT_READ_LIMIT;
@@ -4871,6 +5255,10 @@ async function moveIfExists(from, to) {
4871
5255
  }
4872
5256
  });
4873
5257
  }
5258
+ async function fileExists(filePath) {
5259
+ const info = await stat3(filePath).catch(() => null);
5260
+ return Boolean(info?.isFile());
5261
+ }
4874
5262
  function rotatedLogFile(filePath, index) {
4875
5263
  return `${filePath}.${index}`;
4876
5264
  }
@@ -5415,15 +5803,15 @@ ${stderr}`.trim();
5415
5803
  });
5416
5804
  });
5417
5805
  }
5418
- function assertHermesRunsApiSupported(version, status) {
5806
+ function assertHermesRunsApiSupported(version, status, endpoint = "/v1/runs") {
5419
5807
  if (status !== 404) {
5420
5808
  return;
5421
5809
  }
5422
5810
  const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
5423
5811
  throw new LinkHttpError(
5424
5812
  502,
5425
- "hermes_runs_api_unsupported",
5426
- `${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684 /v1/runs \u63A5\u53E3\uFF0C\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7 Hermes Agent\u3002`
5813
+ endpoint === "/v1/runs" ? "hermes_runs_api_unsupported" : "hermes_api_endpoint_unsupported",
5814
+ `${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684 ${endpoint} \u63A5\u53E3\uFF0C\u8BF7\u5148\u8FD0\u884C \`hermes update\` \u5347\u7EA7 Hermes Agent\u3002`
5427
5815
  );
5428
5816
  }
5429
5817
  async function startHermesGatewayOnce(paths, profileName, logger) {
@@ -5527,7 +5915,7 @@ async function restartHermesGatewayServiceIfAvailable(options) {
5527
5915
  "LaunchAgents",
5528
5916
  "ai.hermes.gateway.plist"
5529
5917
  );
5530
- if (!await fileExists(launchdPlistPath)) {
5918
+ if (!await fileExists2(launchdPlistPath)) {
5531
5919
  return null;
5532
5920
  }
5533
5921
  try {
@@ -5598,7 +5986,7 @@ function buildHermesGatewayChildEnv() {
5598
5986
  }
5599
5987
  return env;
5600
5988
  }
5601
- async function fileExists(filePath) {
5989
+ async function fileExists2(filePath) {
5602
5990
  return access(filePath).then(() => true).catch(() => false);
5603
5991
  }
5604
5992
  async function waitForHermesApiHealth(config, fetcher, timeoutMs) {
@@ -9378,10 +9766,25 @@ var ConversationOrchestrationCoordinator = class {
9378
9766
  if (isConversationNotFoundError(failError)) {
9379
9767
  return;
9380
9768
  }
9381
- throw failError;
9769
+ await this.deps.logger.error("conversation_run_fail_mark_failed", {
9770
+ conversation_id: conversationId,
9771
+ run_id: runId,
9772
+ error: failError instanceof Error ? failError.message : String(failError)
9773
+ });
9382
9774
  }
9383
9775
  }).finally(() => {
9384
- void this.startNextQueuedRun(conversationId);
9776
+ void this.startNextQueuedRun(conversationId).catch((error) => {
9777
+ void this.deps.logger.warn("conversation_queue_drain_failed", {
9778
+ conversation_id: conversationId,
9779
+ error: error instanceof Error ? error.message : String(error)
9780
+ });
9781
+ });
9782
+ }).catch((error) => {
9783
+ void this.deps.logger.error("conversation_run_worker_unhandled", {
9784
+ conversation_id: conversationId,
9785
+ run_id: runId,
9786
+ error: error instanceof Error ? error.message : String(error)
9787
+ });
9385
9788
  });
9386
9789
  }
9387
9790
  async startNextQueuedRunLocked(conversationId) {
@@ -9917,6 +10320,12 @@ function isConversationNotFoundError(error) {
9917
10320
 
9918
10321
  // src/conversations/agent-events.ts
9919
10322
  import { createHash as createHash3 } from "crypto";
10323
+ var APP_TOOL_EVENT_FIELDS_TO_DROP = /* @__PURE__ */ new Set([
10324
+ "output",
10325
+ "content",
10326
+ "result",
10327
+ "message"
10328
+ ]);
9920
10329
  function projectConversationAgentEvent(event) {
9921
10330
  if (!isAgentActivityEvent(event.type)) {
9922
10331
  return null;
@@ -9925,8 +10334,7 @@ function projectConversationAgentEvent(event) {
9925
10334
  type: event.type,
9926
10335
  payload: event.payload ?? {},
9927
10336
  createdAt: event.created_at,
9928
- fallbackKey: String(event.seq),
9929
- raw: event.raw
10337
+ fallbackKey: String(event.seq)
9930
10338
  });
9931
10339
  }
9932
10340
  function projectAgentEvent(input) {
@@ -9942,18 +10350,16 @@ function projectAgentEvent(input) {
9942
10350
  const id = readString7(input.payload, "tool_call_id") ?? readString7(input.payload, "toolCallId") ?? readString7(input.payload, "call_id") ?? readString7(input.payload, "id") ?? readString7(tool, "id") ?? readString7(call, "id") ?? readString7(fn, "id") ?? `tool_${hashAgentEventKey(`${type}:${input.fallbackKey}:${stableStringify(input.payload)}`)}`;
9943
10351
  const rawArguments = input.payload.arguments ?? input.payload.args ?? input.payload.input ?? tool.arguments ?? tool.args ?? tool.input ?? call.arguments ?? call.args ?? call.input ?? fn.arguments;
9944
10352
  const args = stringifyAgentValue(rawArguments);
9945
- const output = stringifyAgentValue(
9946
- input.payload.result ?? input.payload.output ?? input.payload.content ?? input.payload.message ?? tool.result ?? tool.output ?? call.result ?? call.output
9947
- );
9948
10353
  const summary = readString7(input.payload, "summary") ?? readString7(input.payload, "description") ?? readText(input.payload, "preview") ?? readString7(tool, "summary") ?? readString7(tool, "description") ?? readString7(call, "summary") ?? void 0;
9949
10354
  const status = input.payload.error === true ? "failed" : agentStatusForEventType(type);
9950
- const detail = status === "failed" ? readErrorMessage(input.payload) ?? output ?? summary ?? args ?? void 0 : status === "completed" ? output ?? summary ?? args ?? void 0 : args ?? summary ?? void 0;
9951
- const subtitle = summarizeAgentArguments({
10355
+ const actionSummary = summarizeAgentArguments({
9952
10356
  toolName: name,
9953
10357
  rawArguments,
9954
10358
  summary,
9955
10359
  args
9956
- }) ?? (status === "running" ? `\u6B63\u5728\u8C03\u7528 ${name}` : status === "completed" ? `${name} \u5DF2\u5B8C\u6210` : `${name} \u6267\u884C\u5931\u8D25`);
10360
+ });
10361
+ const detail = status === "failed" ? readErrorMessage(input.payload) ?? actionSummary ?? void 0 : actionSummary ?? void 0;
10362
+ const subtitle = actionSummary ?? (status === "running" ? `\u6B63\u5728\u8C03\u7528 ${name}` : status === "completed" ? `${name} \u5DF2\u5B8C\u6210` : `${name} \u6267\u884C\u5931\u8D25`);
9957
10363
  return {
9958
10364
  id,
9959
10365
  title: humanizeToolName(name),
@@ -9963,10 +10369,23 @@ function projectAgentEvent(input) {
9963
10369
  created_at: createdAt,
9964
10370
  ...status === "running" ? {} : {
9965
10371
  completed_at: readString7(input.payload, "completed_at") ?? input.createdAt
9966
- },
9967
- ...input.raw ? { raw: input.raw } : {}
10372
+ }
10373
+ };
10374
+ }
10375
+ function projectAppConversationEvent(event) {
10376
+ const { raw: _raw, ...baseEvent } = event;
10377
+ if (!event.payload) {
10378
+ return baseEvent;
10379
+ }
10380
+ return {
10381
+ ...baseEvent,
10382
+ payload: sanitizeAppConversationEventPayload(event.type, event.payload)
9968
10383
  };
9969
10384
  }
10385
+ function sanitizePersistedAgentEventProjection(event) {
10386
+ const { raw: _raw, ...baseEvent } = event;
10387
+ return baseEvent;
10388
+ }
9970
10389
  function isAgentActivityEvent(type) {
9971
10390
  return type.startsWith("tool.");
9972
10391
  }
@@ -10017,6 +10436,7 @@ function upsertAgentEventProjection(events, next) {
10017
10436
  next.subtitle,
10018
10437
  next.title
10019
10438
  );
10439
+ const keepPreviousTerminalDetail = previous.detail !== void 0 && next.status !== "running" && next.status !== "info";
10020
10440
  const status = next.status === "running" && previous.status !== "running" ? previous.status : next.status;
10021
10441
  const merged = {
10022
10442
  ...previous,
@@ -10026,7 +10446,7 @@ function upsertAgentEventProjection(events, next) {
10026
10446
  title: isGenericToolTitle(next.title) ? previous.title : next.title,
10027
10447
  created_at: earliestTimestamp(previous.created_at, next.created_at),
10028
10448
  subtitle: nextSubtitleIsFallback ? previous.subtitle ?? next.subtitle : next.subtitle ?? previous.subtitle,
10029
- detail: next.detail ?? previous.detail,
10449
+ detail: keepPreviousTerminalDetail ? previous.detail : next.detail ?? previous.detail,
10030
10450
  completed_at: next.completed_at ?? previous.completed_at
10031
10451
  };
10032
10452
  const copy = [...events];
@@ -10053,7 +10473,47 @@ function isToolStatusFallbackSubtitle(value, title) {
10053
10473
  return false;
10054
10474
  }
10055
10475
  const normalized = value.trim().toLowerCase();
10056
- return normalized.startsWith("\u6B63\u5728\u8C03\u7528 ") || normalized.endsWith(" \u5DF2\u5B8C\u6210") || normalized.endsWith(" \u6267\u884C\u5931\u8D25") || normalized === "tool" || normalized === "running";
10476
+ const normalizedTitle = title.trim().toLowerCase();
10477
+ return compactToolStatusText(normalized) === compactToolStatusText(normalizedTitle) || normalized.startsWith("\u6B63\u5728\u8C03\u7528 ") || normalized.endsWith(" \u5DF2\u5B8C\u6210") || normalized.endsWith(" \u6267\u884C\u5931\u8D25") || normalized === "tool" || normalized === "running";
10478
+ }
10479
+ function compactToolStatusText(value) {
10480
+ return value.trim().toLowerCase().replace(/[\s_-]+/gu, "");
10481
+ }
10482
+ function sanitizeAppConversationEventPayload(type, payload) {
10483
+ return sanitizeAppConversationEventValue(payload, {
10484
+ dropRootFields: isToolCompletedEvent(type) ? APP_TOOL_EVENT_FIELDS_TO_DROP : null,
10485
+ depth: 0
10486
+ });
10487
+ }
10488
+ function sanitizeAppConversationEventValue(value, input) {
10489
+ if (Array.isArray(value)) {
10490
+ return value.map(
10491
+ (item) => sanitizeAppConversationEventValue(item, {
10492
+ dropRootFields: input.dropRootFields,
10493
+ depth: input.depth + 1
10494
+ })
10495
+ );
10496
+ }
10497
+ if (value === null || typeof value !== "object") {
10498
+ return value;
10499
+ }
10500
+ const next = {};
10501
+ for (const [key, child] of Object.entries(toRecord6(value))) {
10502
+ if (key === "raw") {
10503
+ continue;
10504
+ }
10505
+ if (input.depth === 0 && input.dropRootFields?.has(key)) {
10506
+ continue;
10507
+ }
10508
+ next[key] = sanitizeAppConversationEventValue(child, {
10509
+ dropRootFields: input.dropRootFields,
10510
+ depth: input.depth + 1
10511
+ });
10512
+ }
10513
+ return next;
10514
+ }
10515
+ function isToolCompletedEvent(type) {
10516
+ return type.toLowerCase() === "tool.completed";
10057
10517
  }
10058
10518
  function closeRunningAgentEvents(events, status, completedAt) {
10059
10519
  if (!events?.length) {
@@ -10088,7 +10548,44 @@ function summarizeAgentArguments(input) {
10088
10548
  if (skillViewSummary) {
10089
10549
  return skillViewSummary;
10090
10550
  }
10091
- return compactAgentSummary(input.summary ?? input.args);
10551
+ const argumentSummary = summarizeGenericToolArguments(
10552
+ input.rawArguments,
10553
+ input.args
10554
+ );
10555
+ if (argumentSummary) {
10556
+ return argumentSummary;
10557
+ }
10558
+ if (input.summary && compactToolText(input.summary) !== compactToolText(input.toolName)) {
10559
+ return compactAgentSummary(input.summary);
10560
+ }
10561
+ return void 0;
10562
+ }
10563
+ function summarizeGenericToolArguments(rawArguments, args) {
10564
+ const argumentObject = readToolArgumentsObject(rawArguments, args);
10565
+ const entries = Object.entries(argumentObject).filter(([, value]) => value !== void 0 && value !== null).slice(0, 3);
10566
+ if (entries.length > 0) {
10567
+ return compactAgentSummary(
10568
+ entries.map(([key, value]) => {
10569
+ const text = stringifyAgentValue(value) ?? "";
10570
+ return `${humanizeArgumentKey(key)}: ${text}`;
10571
+ }).join(" \xB7 ")
10572
+ );
10573
+ }
10574
+ const trimmed = args?.trim();
10575
+ if (!trimmed || looksLikeJsonObject(trimmed)) {
10576
+ return void 0;
10577
+ }
10578
+ return compactAgentSummary(trimmed);
10579
+ }
10580
+ function humanizeArgumentKey(value) {
10581
+ return value.trim().replace(/([a-z0-9])([A-Z])/gu, "$1 $2").replace(/[_-]+/gu, " ").replace(/\s+/gu, " ").toLowerCase();
10582
+ }
10583
+ function compactToolText(value) {
10584
+ return value.trim().toLowerCase().replace(/[\s_-]+/gu, "");
10585
+ }
10586
+ function looksLikeJsonObject(value) {
10587
+ const trimmed = value.trim();
10588
+ return trimmed.startsWith("{") && trimmed.endsWith("}");
10092
10589
  }
10093
10590
  function summarizeSkillViewArguments(toolName, rawArguments, args) {
10094
10591
  const normalizedToolName = toolName.trim().toLowerCase().replace(/[\s-]+/gu, "_");
@@ -10416,9 +10913,7 @@ var ConversationQueryCoordinator = class {
10416
10913
  return message;
10417
10914
  }
10418
10915
  const fromLog = eventsByMessageId.get(message.id) ?? [];
10419
- const persisted = (message.agent_events ?? []).filter(
10420
- isMeaningfulAgentEventProjection
10421
- );
10916
+ const persisted = (message.agent_events ?? []).filter(isMeaningfulAgentEventProjection).map(sanitizePersistedAgentEventProjection);
10422
10917
  const agentEvents = mergeAgentEventProjections(fromLog, persisted);
10423
10918
  return agentEvents.length > 0 ? {
10424
10919
  ...message,
@@ -10503,8 +10998,9 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
10503
10998
  return {
10504
10999
  ...block,
10505
11000
  events: block.events.map((event) => {
11001
+ const appEvent = sanitizePersistedAgentEventProjection(event);
10506
11002
  const hydrated = agentEventById.get(event.id);
10507
- return hydrated ? upsertAgentEventProjection([event], hydrated)[0] ?? event : event;
11003
+ return hydrated ?? appEvent;
10508
11004
  })
10509
11005
  };
10510
11006
  });
@@ -12947,10 +13443,24 @@ async function streamHermesResponses(input, options = {}) {
12947
13443
  options
12948
13444
  );
12949
13445
  }
13446
+ if (response.status === 404 && input.previous_response_id) {
13447
+ const message = await readUpstreamErrorMessage(
13448
+ response,
13449
+ "Previous Hermes response was not found"
13450
+ );
13451
+ if (/previous response not found|response not found/iu.test(message)) {
13452
+ throw new LinkHttpError(
13453
+ 409,
13454
+ "hermes_previous_response_not_found",
13455
+ message
13456
+ );
13457
+ }
13458
+ }
12950
13459
  if (response.status === 404 || response.status === 503) {
12951
13460
  assertHermesRunsApiSupported(
12952
13461
  await readHermesVersion({ logger: options.logger }).catch(() => null),
12953
- response.status
13462
+ response.status,
13463
+ "/v1/responses"
12954
13464
  );
12955
13465
  throw new LinkHttpError(
12956
13466
  503,
@@ -13085,10 +13595,10 @@ function parseHermesApiCapabilities(payload) {
13085
13595
  sessionKeyHeader: readString10(features, "session_key_header")
13086
13596
  };
13087
13597
  }
13088
- async function callHermesApi(path28, init, options) {
13598
+ async function callHermesApi(path29, init, options) {
13089
13599
  const method = init.method ?? "GET";
13090
13600
  const startedAt = Date.now();
13091
- void options.logger?.debug("hermes_api_request_started", { method, path: path28 });
13601
+ void options.logger?.debug("hermes_api_request_started", { method, path: path29 });
13092
13602
  const availability = await ensureHermesApiServerAvailable({
13093
13603
  fetchImpl: options.fetchImpl,
13094
13604
  logger: options.logger,
@@ -13096,7 +13606,7 @@ async function callHermesApi(path28, init, options) {
13096
13606
  });
13097
13607
  let config = availability.configResult.apiServer;
13098
13608
  const fetcher = options.fetchImpl ?? fetch;
13099
- const request = () => fetchHermesApi(fetcher, config, path28, init, options);
13609
+ const request = () => fetchHermesApi(fetcher, config, path29, init, options);
13100
13610
  let response;
13101
13611
  try {
13102
13612
  response = await request();
@@ -13104,7 +13614,7 @@ async function callHermesApi(path28, init, options) {
13104
13614
  logHermesApiError(
13105
13615
  options.logger,
13106
13616
  method,
13107
- path28,
13617
+ path29,
13108
13618
  options.profileName,
13109
13619
  startedAt,
13110
13620
  error
@@ -13115,7 +13625,7 @@ async function callHermesApi(path28, init, options) {
13115
13625
  logHermesApiResponse(
13116
13626
  options.logger,
13117
13627
  method,
13118
- path28,
13628
+ path29,
13119
13629
  options.profileName,
13120
13630
  startedAt,
13121
13631
  response
@@ -13124,7 +13634,7 @@ async function callHermesApi(path28, init, options) {
13124
13634
  }
13125
13635
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
13126
13636
  method,
13127
- path: path28,
13637
+ path: path29,
13128
13638
  profile: options.profileName ?? "default",
13129
13639
  port: config.port ?? null,
13130
13640
  duration_ms: Date.now() - startedAt
@@ -13142,7 +13652,7 @@ async function callHermesApi(path28, init, options) {
13142
13652
  logHermesApiError(
13143
13653
  options.logger,
13144
13654
  method,
13145
- path28,
13655
+ path29,
13146
13656
  options.profileName,
13147
13657
  startedAt,
13148
13658
  error
@@ -13152,7 +13662,7 @@ async function callHermesApi(path28, init, options) {
13152
13662
  logHermesApiResponse(
13153
13663
  options.logger,
13154
13664
  method,
13155
- path28,
13665
+ path29,
13156
13666
  options.profileName,
13157
13667
  startedAt,
13158
13668
  response
@@ -13162,7 +13672,7 @@ async function callHermesApi(path28, init, options) {
13162
13672
  }
13163
13673
  void options.logger?.warn("hermes_api_request_repairing_after_401", {
13164
13674
  method,
13165
- path: path28,
13675
+ path: path29,
13166
13676
  profile: options.profileName ?? "default",
13167
13677
  port: config.port ?? null,
13168
13678
  duration_ms: Date.now() - startedAt
@@ -13182,7 +13692,7 @@ async function callHermesApi(path28, init, options) {
13182
13692
  logHermesApiError(
13183
13693
  options.logger,
13184
13694
  method,
13185
- path28,
13695
+ path29,
13186
13696
  options.profileName,
13187
13697
  startedAt,
13188
13698
  error
@@ -13192,21 +13702,21 @@ async function callHermesApi(path28, init, options) {
13192
13702
  logHermesApiResponse(
13193
13703
  options.logger,
13194
13704
  method,
13195
- path28,
13705
+ path29,
13196
13706
  options.profileName,
13197
13707
  startedAt,
13198
13708
  response
13199
13709
  );
13200
13710
  return response;
13201
13711
  }
13202
- async function fetchHermesApi(fetcher, config, path28, init, options) {
13712
+ async function fetchHermesApi(fetcher, config, path29, init, options) {
13203
13713
  const headers = new Headers(init.headers);
13204
13714
  headers.set("accept", headers.get("accept") ?? "application/json");
13205
13715
  if (config.key) {
13206
13716
  headers.set("x-api-key", config.key);
13207
13717
  headers.set("authorization", `Bearer ${config.key}`);
13208
13718
  }
13209
- return await fetcher(`http://127.0.0.1:${config.port}${path28}`, {
13719
+ return await fetcher(`http://127.0.0.1:${config.port}${path29}`, {
13210
13720
  ...init,
13211
13721
  headers
13212
13722
  }).catch((error) => {
@@ -13215,10 +13725,10 @@ async function fetchHermesApi(fetcher, config, path28, init, options) {
13215
13725
  }
13216
13726
  void options.logger?.warn("hermes_api_server_connect_failed", {
13217
13727
  method: String(init.method ?? "GET").toUpperCase(),
13218
- path: path28,
13728
+ path: path29,
13219
13729
  profile: options.profileName ?? "default",
13220
13730
  port: config.port ?? null,
13221
- url: `http://127.0.0.1:${config.port}${path28}`,
13731
+ url: `http://127.0.0.1:${config.port}${path29}`,
13222
13732
  error: error instanceof Error ? error.message : String(error)
13223
13733
  });
13224
13734
  throw new LinkHttpError(
@@ -13228,10 +13738,10 @@ async function fetchHermesApi(fetcher, config, path28, init, options) {
13228
13738
  );
13229
13739
  });
13230
13740
  }
13231
- function logHermesApiResponse(logger, method, path28, profileName, startedAt, response) {
13741
+ function logHermesApiResponse(logger, method, path29, profileName, startedAt, response) {
13232
13742
  const fields = {
13233
13743
  method,
13234
- path: path28,
13744
+ path: path29,
13235
13745
  profile: profileName ?? "default",
13236
13746
  status: response.status,
13237
13747
  duration_ms: Date.now() - startedAt
@@ -13252,10 +13762,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
13252
13762
  ...upstreamError ? { upstream_error: upstreamError } : {}
13253
13763
  });
13254
13764
  }
13255
- function logHermesApiError(logger, method, path28, profileName, startedAt, error) {
13765
+ function logHermesApiError(logger, method, path29, profileName, startedAt, error) {
13256
13766
  void logger?.warn("hermes_api_request_failed", {
13257
13767
  method,
13258
- path: path28,
13768
+ path: path29,
13259
13769
  profile: profileName ?? "default",
13260
13770
  duration_ms: Date.now() - startedAt,
13261
13771
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
@@ -14078,24 +14588,29 @@ async function* parseSseResponse(response) {
14078
14588
  let buffer = "";
14079
14589
  for await (const chunk of response.body) {
14080
14590
  buffer += decoder.decode(chunk, { stream: true });
14081
- let separatorIndex = buffer.indexOf("\n\n");
14082
- while (separatorIndex >= 0) {
14083
- const block = buffer.slice(0, separatorIndex);
14084
- buffer = buffer.slice(separatorIndex + 2);
14591
+ let separator = findSseBlockSeparator(buffer);
14592
+ while (separator) {
14593
+ const block = buffer.slice(0, separator.index);
14594
+ buffer = buffer.slice(separator.index + separator.length);
14085
14595
  const parsed = parseSseBlock(block);
14086
14596
  if (parsed) {
14087
14597
  yield parsed;
14088
14598
  }
14089
- separatorIndex = buffer.indexOf("\n\n");
14599
+ separator = findSseBlockSeparator(buffer);
14090
14600
  }
14091
14601
  }
14602
+ buffer += decoder.decode();
14092
14603
  const trailing = parseSseBlock(buffer);
14093
14604
  if (trailing) {
14094
14605
  yield trailing;
14095
14606
  }
14096
14607
  }
14608
+ function findSseBlockSeparator(value) {
14609
+ const match = /\r\n\r\n|\n\n|\r\r/u.exec(value);
14610
+ return match ? { index: match.index, length: match[0].length } : null;
14611
+ }
14097
14612
  function parseSseBlock(block) {
14098
- const lines = block.split("\n");
14613
+ const lines = block.split(/\r\n|\n|\r/u);
14099
14614
  let eventName = "";
14100
14615
  const data = [];
14101
14616
  for (const rawLine of lines) {
@@ -14155,7 +14670,7 @@ function resolveConversationRunBackend(env = process.env) {
14155
14670
  if (RUNS_BACKEND_VALUES.has(raw)) {
14156
14671
  return "runs";
14157
14672
  }
14158
- return "runs";
14673
+ return "responses";
14159
14674
  }
14160
14675
  function isRunToolResultCompensationEnabled(env = process.env) {
14161
14676
  const raw = env.HERMESLINK_RUN_TOOL_RESULT_COMPENSATION?.trim().toLowerCase();
@@ -15158,7 +15673,14 @@ var ConversationRunLifecycle = class {
15158
15673
  runId,
15159
15674
  hermesSessionId
15160
15675
  );
15161
- const conversationHistory = await buildConversationHistory({
15676
+ const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
15677
+ if (previousResponseId) {
15678
+ await this.updateRun(conversationId, runId, {
15679
+ previous_response_id: previousResponseId
15680
+ });
15681
+ }
15682
+ const shouldBuildConversationHistory = backend === "runs" || !previousResponseId;
15683
+ let conversationHistory = shouldBuildConversationHistory ? await buildConversationHistory({
15162
15684
  paths: this.deps.paths,
15163
15685
  profileName: run.profile,
15164
15686
  hermesSessionId,
@@ -15175,29 +15697,31 @@ var ConversationRunLifecycle = class {
15175
15697
  source: "empty",
15176
15698
  diagnostics: emptyConversationHistoryDiagnostics("build_failed")
15177
15699
  };
15178
- });
15179
- await this.deps.logger.debug("conversation_history_built", {
15180
- conversation_id: conversationId,
15181
- run_id: runId,
15182
- source: conversationHistory.source,
15183
- message_count: conversationHistory.messages.length,
15184
- ...conversationHistory.diagnostics
15185
- });
15186
- const cronJobIdsBeforeRun = await this.readHermesCronJobIds(
15187
- run.profile
15188
- ).catch(() => null);
15189
- const resolvedInput = await this.resolveRunInput({
15700
+ }) : {
15701
+ messages: [],
15702
+ source: "empty",
15703
+ diagnostics: emptyConversationHistoryDiagnostics("no_history")
15704
+ };
15705
+ await this.deps.logger.debug(
15706
+ shouldBuildConversationHistory ? "conversation_history_built" : "conversation_history_skipped_previous_response",
15707
+ {
15708
+ conversation_id: conversationId,
15709
+ run_id: runId,
15710
+ backend,
15711
+ source: conversationHistory.source,
15712
+ message_count: conversationHistory.messages.length,
15713
+ ...conversationHistory.diagnostics
15714
+ }
15715
+ );
15716
+ const cronJobIdsBeforeRun = await this.readHermesCronJobIds(
15717
+ run.profile
15718
+ ).catch(() => null);
15719
+ const resolvedInput = await this.resolveRunInput({
15190
15720
  conversationId,
15191
15721
  run,
15192
15722
  fallbackInput: input,
15193
15723
  snapshot
15194
15724
  });
15195
- const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
15196
- if (previousResponseId) {
15197
- await this.updateRun(conversationId, runId, {
15198
- previous_response_id: previousResponseId
15199
- });
15200
- }
15201
15725
  const deliveryStagingDir = await prepareDeliveryStagingRunDir(
15202
15726
  this.deps.paths,
15203
15727
  conversationId,
@@ -15211,12 +15735,12 @@ var ConversationRunLifecycle = class {
15211
15735
  return void 0;
15212
15736
  });
15213
15737
  const instructions = buildRunInstructions(run, deliveryStagingDir);
15214
- const estimatedUsage = estimateContextUsage({
15738
+ const estimatedUsage = shouldBuildConversationHistory ? estimateContextUsage({
15215
15739
  conversationHistory: conversationHistory.messages,
15216
15740
  currentInput: resolvedInput,
15217
15741
  instructions,
15218
15742
  contextWindow: run.context_window
15219
- });
15743
+ }) : void 0;
15220
15744
  if (estimatedUsage) {
15221
15745
  await this.updateRun(conversationId, runId, { usage: estimatedUsage });
15222
15746
  }
@@ -15225,22 +15749,89 @@ var ConversationRunLifecycle = class {
15225
15749
  run.profile ?? "default"
15226
15750
  );
15227
15751
  if (backend === "responses") {
15228
- const response = await streamHermesResponses(
15229
- {
15230
- input: resolvedInput,
15231
- instructions,
15232
- session_id: hermesSessionId,
15233
- session_key: sessionKey,
15234
- model: run.model,
15235
- ...previousResponseId ? { previous_response_id: previousResponseId } : {},
15236
- ...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
15237
- },
15238
- {
15239
- logger: this.deps.logger,
15240
- profileName: run.profile,
15241
- signal: controller.signal
15752
+ let response;
15753
+ try {
15754
+ response = await streamHermesResponses(
15755
+ {
15756
+ input: resolvedInput,
15757
+ instructions,
15758
+ session_id: hermesSessionId,
15759
+ session_key: sessionKey,
15760
+ model: run.model,
15761
+ ...previousResponseId ? { previous_response_id: previousResponseId } : {},
15762
+ ...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
15763
+ },
15764
+ {
15765
+ logger: this.deps.logger,
15766
+ profileName: run.profile,
15767
+ signal: controller.signal
15768
+ }
15769
+ );
15770
+ } catch (error) {
15771
+ if (!previousResponseId || !isLinkHttpError(error) || error.code !== "hermes_previous_response_not_found") {
15772
+ throw error;
15242
15773
  }
15243
- );
15774
+ await this.deps.logger.warn(
15775
+ "hermes_previous_response_missing_falling_back_to_history",
15776
+ {
15777
+ conversation_id: conversationId,
15778
+ run_id: runId,
15779
+ previous_response_id: previousResponseId,
15780
+ error: error.message
15781
+ }
15782
+ );
15783
+ await this.updateRun(conversationId, runId, {
15784
+ previous_response_id: void 0
15785
+ });
15786
+ conversationHistory = await buildConversationHistory({
15787
+ paths: this.deps.paths,
15788
+ profileName: run.profile,
15789
+ hermesSessionId,
15790
+ snapshot,
15791
+ run
15792
+ }).catch(async (buildError) => {
15793
+ await this.deps.logger.warn("conversation_history_build_failed", {
15794
+ conversation_id: conversationId,
15795
+ run_id: runId,
15796
+ error: buildError instanceof Error ? buildError.message : String(buildError)
15797
+ });
15798
+ return {
15799
+ messages: [],
15800
+ source: "empty",
15801
+ diagnostics: emptyConversationHistoryDiagnostics("build_failed")
15802
+ };
15803
+ });
15804
+ await this.deps.logger.debug("conversation_history_built", {
15805
+ conversation_id: conversationId,
15806
+ run_id: runId,
15807
+ backend,
15808
+ source: conversationHistory.source,
15809
+ message_count: conversationHistory.messages.length,
15810
+ ...conversationHistory.diagnostics
15811
+ });
15812
+ const fallbackUsage = estimateContextUsage({
15813
+ conversationHistory: conversationHistory.messages,
15814
+ currentInput: resolvedInput,
15815
+ instructions,
15816
+ contextWindow: run.context_window
15817
+ });
15818
+ await this.updateRun(conversationId, runId, { usage: fallbackUsage });
15819
+ response = await streamHermesResponses(
15820
+ {
15821
+ input: resolvedInput,
15822
+ instructions,
15823
+ session_id: hermesSessionId,
15824
+ session_key: sessionKey,
15825
+ model: run.model,
15826
+ ...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
15827
+ },
15828
+ {
15829
+ logger: this.deps.logger,
15830
+ profileName: run.profile,
15831
+ signal: controller.signal
15832
+ }
15833
+ );
15834
+ }
15244
15835
  const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
15245
15836
  if (responseSessionId) {
15246
15837
  await this.rememberRunHermesSessionId(
@@ -15336,6 +15927,7 @@ var ConversationRunLifecycle = class {
15336
15927
  );
15337
15928
  }
15338
15929
  async failRun(conversationId, runId, message, source) {
15930
+ await this.refreshRunHermesCompressionTip(conversationId, runId);
15339
15931
  return this.deps.withConversationLock(
15340
15932
  conversationId,
15341
15933
  () => this.failRunLocked(conversationId, runId, message, source)
@@ -15433,9 +16025,21 @@ var ConversationRunLifecycle = class {
15433
16025
  await this.cancelRunAfterAbort(input.conversationId, input.runId);
15434
16026
  return;
15435
16027
  }
15436
- if (input.backend === "responses" && !streamError && await this.runHasAssistantOutput(input.conversationId, input.runId)) {
16028
+ const hasAssistantOutput = await this.runHasAssistantOutput(
16029
+ input.conversationId,
16030
+ input.runId
16031
+ );
16032
+ if (input.backend === "responses" && !streamError && hasAssistantOutput) {
15437
16033
  await this.completeRun(input.conversationId, input.runId);
15438
16034
  } else {
16035
+ await this.deps.logger.warn("hermes_event_stream_ended_without_terminal", {
16036
+ backend: input.backend,
16037
+ conversation_id: input.conversationId,
16038
+ run_id: input.runId,
16039
+ ...input.hermesRunId ? { hermes_run_id: input.hermesRunId } : {},
16040
+ has_assistant_output: hasAssistantOutput,
16041
+ ...streamError ? { error: formatUnknownErrorMessage(streamError) } : {}
16042
+ });
15439
16043
  await this.failRun(
15440
16044
  input.conversationId,
15441
16045
  input.runId,
@@ -16053,11 +16657,38 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
16053
16657
  return user ? messageRequestsAppDelivery(messageText(user)) : false;
16054
16658
  }
16055
16659
  async completeRun(conversationId, runId, source) {
16660
+ await this.refreshRunHermesCompressionTip(conversationId, runId);
16056
16661
  return this.deps.withConversationLock(
16057
16662
  conversationId,
16058
16663
  () => this.completeRunLocked(conversationId, runId, source)
16059
16664
  );
16060
16665
  }
16666
+ async refreshRunHermesCompressionTip(conversationId, runId) {
16667
+ const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
16668
+ const run = snapshot?.runs.find((item) => item.id === runId);
16669
+ if (!run?.hermes_session_id) {
16670
+ return;
16671
+ }
16672
+ const compressionTip = await readHermesCompressionTip(
16673
+ run.hermes_session_id,
16674
+ this.deps.paths,
16675
+ run.profile
16676
+ ).catch(() => void 0);
16677
+ if (!compressionTip || compressionTip === run.hermes_session_id) {
16678
+ return;
16679
+ }
16680
+ await this.deps.logger.info("hermes_compression_tip_detected", {
16681
+ conversation_id: conversationId,
16682
+ run_id: runId,
16683
+ previous_hermes_session_id: run.hermes_session_id,
16684
+ hermes_session_id: compressionTip
16685
+ });
16686
+ await this.rememberRunHermesSessionId(
16687
+ conversationId,
16688
+ runId,
16689
+ compressionTip
16690
+ );
16691
+ }
16061
16692
  async completeRunLocked(conversationId, runId, source) {
16062
16693
  let snapshot = await this.deps.readSnapshot(conversationId);
16063
16694
  let run = snapshot.runs.find((item) => item.id === runId);
@@ -16744,6 +17375,7 @@ var ConversationService = class {
16744
17375
  metadata: this.metadata,
16745
17376
  commandHandlers: this.commandHandlers,
16746
17377
  runLifecycle: this.runLifecycle,
17378
+ logger: this.logger,
16747
17379
  withConversationLock: (conversationId, task) => this.withConversationLock(conversationId, task),
16748
17380
  appendEvent: (conversationId, input) => this.appendEvent(conversationId, input),
16749
17381
  resolveMessageAttachmentParts: (conversationId, attachments) => this.maintenance.resolveMessageAttachmentParts(
@@ -17262,7 +17894,14 @@ var ConversationService = class {
17262
17894
  reason: "cancelled by app"
17263
17895
  });
17264
17896
  if (result.run.status === "cancelled") {
17265
- void this.orchestration.startNextQueuedRun(conversationId);
17897
+ void this.orchestration.startNextQueuedRun(conversationId).catch(
17898
+ (error) => {
17899
+ void this.logger.warn("conversation_queue_drain_failed", {
17900
+ conversation_id: conversationId,
17901
+ error: error instanceof Error ? error.message : String(error)
17902
+ });
17903
+ }
17904
+ );
17266
17905
  }
17267
17906
  return result;
17268
17907
  }
@@ -18282,6 +18921,7 @@ function isLanHost(hostname) {
18282
18921
  // src/http/sse.ts
18283
18922
  var DEFAULT_SSE_RETRY_MS = 1e3;
18284
18923
  var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
18924
+ var activeSseSockets = /* @__PURE__ */ new WeakSet();
18285
18925
  function beginSseStream(request, response, options = {}) {
18286
18926
  const retryMs = normalizeRetryMs(options.retryMs);
18287
18927
  const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
@@ -18289,11 +18929,15 @@ function beginSseStream(request, response, options = {}) {
18289
18929
  response.setHeader("content-type", "text/event-stream; charset=utf-8");
18290
18930
  response.setHeader("cache-control", "no-store");
18291
18931
  response.setHeader("connection", "keep-alive");
18932
+ activeSseSockets.add(request.socket);
18292
18933
  response.flushHeaders();
18293
18934
  writeSseRetry(response, retryMs);
18294
18935
  writeSseComment(response, options.initialComment ?? "connected");
18295
18936
  let closed = false;
18296
18937
  let heartbeat = null;
18938
+ const onStreamError = () => {
18939
+ cleanup();
18940
+ };
18297
18941
  const cleanup = () => {
18298
18942
  if (closed) {
18299
18943
  return;
@@ -18305,9 +18949,18 @@ function beginSseStream(request, response, options = {}) {
18305
18949
  }
18306
18950
  request.off("close", cleanup);
18307
18951
  response.off("close", cleanup);
18952
+ request.off("error", onStreamError);
18953
+ response.off("error", onStreamError);
18954
+ activeSseSockets.delete(request.socket);
18308
18955
  options.onClose?.();
18309
18956
  if (!response.writableEnded && !response.destroyed) {
18310
- response.end();
18957
+ try {
18958
+ response.end();
18959
+ } catch (error) {
18960
+ if (!isExpectedClientDisconnectError(error)) {
18961
+ throw error;
18962
+ }
18963
+ }
18311
18964
  }
18312
18965
  };
18313
18966
  heartbeat = setInterval(() => {
@@ -18320,37 +18973,43 @@ function beginSseStream(request, response, options = {}) {
18320
18973
  heartbeat.unref();
18321
18974
  request.once("close", cleanup);
18322
18975
  response.once("close", cleanup);
18976
+ request.once("error", onStreamError);
18977
+ response.once("error", onStreamError);
18323
18978
  return cleanup;
18324
18979
  }
18980
+ function isActiveSseSocket(socket) {
18981
+ return socket != null && activeSseSockets.has(socket);
18982
+ }
18325
18983
  function writeSseEvent(response, event) {
18984
+ const appEvent = projectAppConversationEvent(event);
18326
18985
  writeJsonSseEvent(response, {
18327
- event: event.type,
18328
- data: event,
18329
- id: event.seq
18986
+ event: appEvent.type,
18987
+ data: appEvent,
18988
+ id: appEvent.seq
18330
18989
  });
18331
18990
  }
18332
18991
  function writeJsonSseEvent(response, event) {
18333
18992
  if (event.retryMs != null) {
18334
- response.write(`retry: ${normalizeRetryMs(event.retryMs)}
18993
+ writeResponse(response, `retry: ${normalizeRetryMs(event.retryMs)}
18335
18994
  `);
18336
18995
  }
18337
18996
  if (event.id != null && event.id !== "") {
18338
- response.write(`id: ${event.id}
18997
+ writeResponse(response, `id: ${event.id}
18339
18998
  `);
18340
18999
  }
18341
- response.write(`event: ${event.event}
19000
+ writeResponse(response, `event: ${event.event}
18342
19001
  `);
18343
- response.write(`data: ${JSON.stringify(event.data)}
19002
+ writeResponse(response, `data: ${JSON.stringify(event.data)}
18344
19003
 
18345
19004
  `);
18346
19005
  }
18347
19006
  function writeSseComment(response, comment = "keep-alive") {
18348
- response.write(`: ${comment}
19007
+ writeResponse(response, `: ${comment}
18349
19008
 
18350
19009
  `);
18351
19010
  }
18352
19011
  function writeSseRetry(response, retryMs) {
18353
- response.write(`retry: ${normalizeRetryMs(retryMs)}
19012
+ writeResponse(response, `retry: ${normalizeRetryMs(retryMs)}
18354
19013
 
18355
19014
  `);
18356
19015
  }
@@ -18358,6 +19017,25 @@ function normalizeRetryMs(retryMs) {
18358
19017
  const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
18359
19018
  return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
18360
19019
  }
19020
+ function writeResponse(response, chunk) {
19021
+ if (response.writableEnded || response.destroyed) {
19022
+ return;
19023
+ }
19024
+ try {
19025
+ response.write(chunk);
19026
+ } catch (error) {
19027
+ if (!isExpectedClientDisconnectError(error)) {
19028
+ throw error;
19029
+ }
19030
+ }
19031
+ }
19032
+ function isExpectedClientDisconnectError(error) {
19033
+ if (!(error instanceof Error)) {
19034
+ return false;
19035
+ }
19036
+ const code = String(error.code ?? "");
19037
+ return code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || code === "ETIMEDOUT" || /(?:socket hang up|aborted|write after end)/iu.test(error.message);
19038
+ }
18361
19039
 
18362
19040
  // src/http/routes/conversations.ts
18363
19041
  function registerConversationRoutes(router, options) {
@@ -18838,12 +19516,22 @@ function encodeRfc5987Value(value) {
18838
19516
  }
18839
19517
 
18840
19518
  // src/http/middleware/error-handler.ts
19519
+ var INTERNAL_HEALTH_PROBE_HEADER = "x-hermes-link-internal-health-probe";
18841
19520
  function createHttpErrorMiddleware(logger) {
18842
19521
  return async (ctx, next) => {
18843
19522
  const startedAt = Date.now();
19523
+ const shouldSkipRequestLog = isInternalHealthProbe(ctx);
19524
+ let expectedClientDisconnect = false;
18844
19525
  try {
18845
19526
  await next();
18846
19527
  } catch (error) {
19528
+ if (isExpectedClientDisconnectError2(error, {
19529
+ sse: isSseRequestContext(ctx)
19530
+ })) {
19531
+ expectedClientDisconnect = true;
19532
+ ctx.respond = false;
19533
+ return;
19534
+ }
18847
19535
  const profileError = error instanceof Error && error.message === "invalid profile name";
18848
19536
  const profileNotFound = error instanceof Error && error.message === "profile does not exist";
18849
19537
  const status = isLinkHttpError(error) ? error.status : profileError ? 400 : profileNotFound ? 404 : 500;
@@ -18856,28 +19544,57 @@ function createHttpErrorMiddleware(logger) {
18856
19544
  message: error instanceof Error ? error.message : "Internal error"
18857
19545
  }
18858
19546
  };
18859
- void logger.write(
18860
- status >= 500 ? "error" : "warn",
18861
- "http_request_failed",
18862
- {
19547
+ if (!shouldSkipRequestLog) {
19548
+ void logger.write(
19549
+ status >= 500 ? "error" : "warn",
19550
+ "http_request_failed",
19551
+ {
19552
+ method: ctx.method,
19553
+ path: ctx.path,
19554
+ query: ctx.querystring || null,
19555
+ status,
19556
+ code,
19557
+ error: error instanceof Error ? error.message : String(error)
19558
+ }
19559
+ );
19560
+ }
19561
+ } finally {
19562
+ if (!shouldSkipRequestLog && !expectedClientDisconnect) {
19563
+ void logger.info("http_request", {
18863
19564
  method: ctx.method,
18864
19565
  path: ctx.path,
18865
- query: ctx.querystring || null,
18866
- status,
18867
- code,
18868
- error: error instanceof Error ? error.message : String(error)
18869
- }
18870
- );
18871
- } finally {
18872
- void logger.info("http_request", {
18873
- method: ctx.method,
18874
- path: ctx.path,
18875
- status: ctx.status,
18876
- duration_ms: Date.now() - startedAt
18877
- });
19566
+ status: ctx.status,
19567
+ duration_ms: Date.now() - startedAt
19568
+ });
19569
+ }
18878
19570
  }
18879
19571
  };
18880
19572
  }
19573
+ function isInternalHealthProbe(ctx) {
19574
+ return ctx.path === "/api/v1/bootstrap" && ctx.get(INTERNAL_HEALTH_PROBE_HEADER) === "1";
19575
+ }
19576
+ function isSseRequestContext(ctx) {
19577
+ if (!ctx) {
19578
+ return false;
19579
+ }
19580
+ return isSseRequestPath(ctx.path) || isActiveSseSocket(ctx.req.socket);
19581
+ }
19582
+ function isSseRequestPath(path29) {
19583
+ if (!path29) {
19584
+ return false;
19585
+ }
19586
+ return path29 === "/api/v1/conversations/events" || path29 === "/api/v1/profile-creation/events" || path29 === "/api/v1/hermes/update/events" || path29 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path29) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path29);
19587
+ }
19588
+ function isExpectedClientDisconnectError2(error, options = {}) {
19589
+ if (!(error instanceof Error)) {
19590
+ return false;
19591
+ }
19592
+ const code = String(error.code ?? "");
19593
+ if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted|write after end)/iu.test(error.message)) {
19594
+ return true;
19595
+ }
19596
+ return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
19597
+ }
18881
19598
 
18882
19599
  // src/hermes/profiles.ts
18883
19600
  import { execFile as execFile4 } from "child_process";
@@ -19855,7 +20572,12 @@ function readModelConfigInput(body) {
19855
20572
  function readModelDefaultsInput(body) {
19856
20573
  return {
19857
20574
  taskModelId: readString16(body, "task_model_id") ?? readString16(body, "taskModelId") ?? readString16(body, "default_model_id") ?? readString16(body, "defaultModelId") ?? void 0,
19858
- compressionModelId: readString16(body, "compression_model_id") ?? readString16(body, "compressionModelId") ?? void 0
20575
+ taskModelProvider: readString16(body, "task_model_provider") ?? readString16(body, "taskModelProvider") ?? readString16(body, "default_model_provider") ?? readString16(body, "defaultModelProvider") ?? void 0,
20576
+ taskModelBaseUrl: readString16(body, "task_model_base_url") ?? readString16(body, "taskModelBaseUrl") ?? readString16(body, "default_model_base_url") ?? readString16(body, "defaultModelBaseUrl") ?? void 0,
20577
+ compressionModelId: readString16(body, "compression_model_id") ?? readString16(body, "compressionModelId") ?? void 0,
20578
+ compressionModelProvider: readString16(body, "compression_model_provider") ?? readString16(body, "compressionModelProvider") ?? void 0,
20579
+ compressionModelBaseUrl: readString16(body, "compression_model_base_url") ?? readString16(body, "compressionModelBaseUrl") ?? void 0,
20580
+ reasoningEffort: readString16(body, "reasoning_effort") ?? readString16(body, "reasoningEffort") ?? readString16(body, "default_reasoning_effort") ?? readString16(body, "defaultReasoningEffort") ?? void 0
19859
20581
  };
19860
20582
  }
19861
20583
  function readModelConfigImportInput(body) {
@@ -19958,85 +20680,392 @@ import { EventEmitter as EventEmitter2 } from "events";
19958
20680
  import {
19959
20681
  cp,
19960
20682
  mkdir as mkdir11,
19961
- readFile as readFile14,
20683
+ readFile as readFile15,
19962
20684
  rm as rm6,
19963
- stat as stat14
20685
+ stat as stat15
19964
20686
  } from "fs/promises";
20687
+ import path22 from "path";
20688
+ import YAML4 from "yaml";
20689
+
20690
+ // src/hermes/link-skill.ts
20691
+ import { readFile as readFile14, stat as stat14 } from "fs/promises";
20692
+ import os4 from "os";
19965
20693
  import path21 from "path";
19966
20694
  import YAML3 from "yaml";
19967
- var PROFILE_CREATE_LOG_FILE = "profile-create.log";
19968
- var PROFILE_CREATE_LOG_MAX_FILES = 3;
19969
- var MAX_PROFILE_CREATE_LOG_LINES = 260;
19970
- var MAX_OUTPUT_LINE_LENGTH = 1200;
19971
- var PROFILE_NAME_PATTERN5 = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
19972
- var ALL_COPY_SCOPES = [
19973
- "models",
19974
- "skills",
19975
- "tool_permissions",
19976
- "approval_policy"
19977
- ];
19978
- var PROFILE_CREATION_EVENTS = new EventEmitter2();
19979
- var runningProfileCreation = null;
19980
- async function startHermesProfileCreation(input, options) {
19981
- const current = await readHermesProfileCreationStatus(options.paths);
19982
- if (runningProfileCreation || current.state === "running") {
19983
- return current;
20695
+ var HERMES_LINK_SKILL_ROOT_DIR = "hermes-skills";
20696
+ var HERMES_LINK_SKILL_DIR = "hermes-link";
20697
+ var HERMES_LINK_SKILL_FILE = "SKILL.md";
20698
+ var HERMES_LINK_SKILL_CONTENT = `---
20699
+ name: hermes-link
20700
+ description: Understand and troubleshoot Hermes Link, the local companion service that connects Hermes Agent to the HermesPilot mobile app.
20701
+ ---
20702
+
20703
+ # Hermes Link
20704
+
20705
+ Hermes Link is a secure local companion service for hermes-agent. It lets the HermesPilot mobile app connect to the user's local Hermes Agent safely and reliably.
20706
+
20707
+ Hermes Link is specifically for HermesPilot. It is not OpenClaw, ClawPilot, clawlink, or any OpenClaw companion service. Do not confuse those systems when helping the user.
20708
+
20709
+ ## What Hermes Link Does
20710
+
20711
+ Hermes Link can:
20712
+
20713
+ - pair the HermesPilot mobile app with this computer
20714
+ - expose a controlled local API for the app
20715
+ - connect through LAN, public direct routes, or Hermes Relay
20716
+ - help the app access Hermes profiles, conversations, messages, files, voice input, logs, and status
20717
+ - start or check the Hermes Gateway when needed
20718
+
20719
+ Hermes Link does not replace Hermes Agent. Hermes Agent remains the actual AI agent runtime.
20720
+
20721
+ ## When To Use This Skill
20722
+
20723
+ Use this skill when the user asks about:
20724
+
20725
+ - HermesPilot App cannot connect
20726
+ - Link appears offline
20727
+ - pairing or QR code problems
20728
+ - mobile access to local Hermes
20729
+ - Relay, LAN, or public direct route problems
20730
+ - Hermes Link logs, daemon status, or diagnostics
20731
+
20732
+ ## Useful Commands
20733
+
20734
+ Start with:
20735
+
20736
+ \`\`\`bash
20737
+ hermeslink status
20738
+ hermeslink doctor
20739
+ \`\`\`
20740
+
20741
+ For logs:
20742
+
20743
+ \`\`\`bash
20744
+ hermeslink logs --error -n 50
20745
+ hermeslink logs --warn -n 100
20746
+ hermeslink logs -f
20747
+ hermeslink logs --all --level debug -f
20748
+ \`\`\`
20749
+
20750
+ If the daemon appears stuck, suggest:
20751
+
20752
+ \`\`\`bash
20753
+ hermeslink restart
20754
+ \`\`\`
20755
+
20756
+ Explain that restarting Hermes Link may briefly disconnect the mobile app.
20757
+
20758
+ ## Troubleshooting Flow
20759
+
20760
+ 1. Check \`hermeslink status\`.
20761
+ 2. Run \`hermeslink doctor\`.
20762
+ 3. Inspect recent errors with \`hermeslink logs --error -n 50\`.
20763
+ 4. If the issue is intermittent, reproduce it while running \`hermeslink logs -f\`.
20764
+ 5. Check whether the phone and computer are on the same LAN, using Relay, or using a public direct route.
20765
+ 6. If the user uses WSL, Docker, or a VM, verify that Hermes Agent and Hermes Link run in the same environment.
20766
+
20767
+ ## Safety
20768
+
20769
+ Never reveal API keys, access tokens, refresh tokens, private keys, or full .env contents.
20770
+
20771
+ Do not recommend exposing port 52379 directly to the public internet without TLS, VPN, Tailscale, WireGuard, or another access-control layer.
20772
+
20773
+ Do not modify Hermes profiles, delete user data, edit config files, or kill processes unless the user explicitly asks.
20774
+ `;
20775
+ async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
20776
+ const paths = options.paths ?? resolveRuntimePaths();
20777
+ const externalDir = resolveHermesLinkSkillExternalDir(paths);
20778
+ const skillPath = path21.join(
20779
+ externalDir,
20780
+ HERMES_LINK_SKILL_DIR,
20781
+ HERMES_LINK_SKILL_FILE
20782
+ );
20783
+ const skillChanged = await writeHermesLinkSkill(skillPath);
20784
+ const profiles = await listHermesProfiles(paths);
20785
+ const results = [];
20786
+ for (const profile of profiles) {
20787
+ try {
20788
+ results.push(await ensureProfileUsesExternalSkillDir(profile, externalDir));
20789
+ } catch (error) {
20790
+ const message = error instanceof Error ? error.message : String(error);
20791
+ results.push({
20792
+ profile: profile.name,
20793
+ profilePath: profile.path,
20794
+ configPath: profile.configPath,
20795
+ changed: false,
20796
+ backupPath: null,
20797
+ skipped: false,
20798
+ error: message
20799
+ });
20800
+ }
19984
20801
  }
19985
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
19986
- const normalized = await normalizeProfileCreationInput(input, options.paths);
19987
- const profileName = normalized.name ?? await generateProfileName(normalized.displayName, options.paths);
19988
- const sourceProfile = normalized.copyFrom;
19989
- const copyScopes = normalized.copyScopes;
19990
- const jobId = `profile_create_${now().getTime().toString(36)}`;
19991
- await clearProfileCreationLogFiles(options.paths);
19992
- const writer = createRotatingTextLogWriter({
19993
- paths: options.paths,
19994
- fileName: PROFILE_CREATE_LOG_FILE,
19995
- maxFileBytes: 512 * 1024,
19996
- maxFiles: PROFILE_CREATE_LOG_MAX_FILES
19997
- });
19998
- const startedAt = now().toISOString();
19999
- const started = {
20000
- state: "running",
20001
- job_id: jobId,
20002
- profile_name: profileName,
20003
- source_profile: sourceProfile,
20004
- copied_scopes: [],
20005
- profile: null,
20006
- pid: null,
20007
- started_at: startedAt,
20008
- finished_at: null,
20009
- exit_code: null,
20010
- signal: null,
20011
- error: null
20802
+ const changedProfiles = results.filter((result) => result.changed);
20803
+ const failedProfiles = results.filter((result) => result.error);
20804
+ if (skillChanged || changedProfiles.length > 0) {
20805
+ void options.logger?.info("hermes_link_skill_ensured", {
20806
+ source: options.source ?? "unspecified",
20807
+ external_dir: externalDir,
20808
+ skill_path: skillPath,
20809
+ skill_changed: skillChanged,
20810
+ changed_profiles: changedProfiles.map((result) => result.profile),
20811
+ failed_profiles: failedProfiles.map((result) => result.profile)
20812
+ });
20813
+ }
20814
+ if (failedProfiles.length > 0) {
20815
+ void options.logger?.warn("hermes_link_skill_profile_ensure_failed", {
20816
+ source: options.source ?? "unspecified",
20817
+ profiles: failedProfiles.map((result) => ({
20818
+ profile: result.profile,
20819
+ error: result.error ?? "unknown error"
20820
+ }))
20821
+ });
20822
+ }
20823
+ return {
20824
+ externalDir,
20825
+ skillPath,
20826
+ skillChanged,
20827
+ profiles: results
20012
20828
  };
20013
- await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
20014
- await writeProfileCreationState(options.paths, started);
20015
- await writer.write(`
20016
- === profile creation started ${startedAt} ===
20017
- `);
20018
- await writer.write(`Profile ID: ${profileName}
20019
- `);
20020
- if (sourceProfile && copyScopes.length > 0) {
20021
- await writer.write(
20022
- `Copy from: ${sourceProfile} (${copyScopes.join(", ")})
20023
- `
20024
- );
20025
- } else {
20026
- await writer.write("Copy from: none\n");
20829
+ }
20830
+ async function ensureHermesLinkSkillInstalledBestEffort(options = {}) {
20831
+ try {
20832
+ await ensureHermesLinkSkillInstalledForProfiles(options);
20833
+ } catch (error) {
20834
+ void options.logger?.warn("hermes_link_skill_ensure_failed", {
20835
+ source: options.source ?? "unspecified",
20836
+ error: error instanceof Error ? error.message : String(error)
20837
+ });
20027
20838
  }
20028
- const commandArgs = ["profile", "create", profileName, "--no-alias"];
20029
- await writer.write(`$ ${resolveHermesBin()} ${commandArgs.join(" ")}
20030
- `);
20031
- await emitProfileCreationStatus(options.paths);
20032
- const child = spawn2(resolveHermesBin(), commandArgs, {
20033
- stdio: ["ignore", "pipe", "pipe"],
20034
- env: { ...process.env, HERMES_NONINTERACTIVE: "1" },
20035
- windowsHide: true,
20036
- detached: false
20839
+ }
20840
+ function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
20841
+ return path21.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
20842
+ }
20843
+ async function writeHermesLinkSkill(skillPath) {
20844
+ const existing = await readFile14(skillPath, "utf8").catch((error) => {
20845
+ if (isNodeError16(error, "ENOENT")) {
20846
+ return null;
20847
+ }
20848
+ throw error;
20037
20849
  });
20038
- started.pid = child.pid ?? null;
20039
- await writeProfileCreationState(options.paths, started);
20850
+ if (existing === HERMES_LINK_SKILL_CONTENT) {
20851
+ return false;
20852
+ }
20853
+ await atomicWriteFilePreservingMetadata(skillPath, HERMES_LINK_SKILL_CONTENT);
20854
+ return true;
20855
+ }
20856
+ async function ensureProfileUsesExternalSkillDir(profile, externalDir) {
20857
+ const profilePath = resolveHermesProfileDir(profile.name);
20858
+ const configPath = resolveHermesConfigPath(profile.name);
20859
+ if (!await pathIsDirectory(profilePath)) {
20860
+ return {
20861
+ profile: profile.name,
20862
+ profilePath,
20863
+ configPath,
20864
+ changed: false,
20865
+ backupPath: null,
20866
+ skipped: true
20867
+ };
20868
+ }
20869
+ const { document, config, existingRaw } = await readHermesConfigDocument2(configPath);
20870
+ const skillsConfig = ensureRecord2(config, "skills");
20871
+ if (externalDirsInclude(skillsConfig.external_dirs, externalDir, profilePath)) {
20872
+ return {
20873
+ profile: profile.name,
20874
+ profilePath,
20875
+ configPath,
20876
+ changed: false,
20877
+ backupPath: null,
20878
+ skipped: false
20879
+ };
20880
+ }
20881
+ skillsConfig.external_dirs = appendExternalDir(
20882
+ skillsConfig.external_dirs,
20883
+ externalDir,
20884
+ profilePath
20885
+ );
20886
+ const backupPath = await writeHermesConfigDocument2({
20887
+ configPath,
20888
+ document,
20889
+ config,
20890
+ existingRaw
20891
+ });
20892
+ return {
20893
+ profile: profile.name,
20894
+ profilePath,
20895
+ configPath,
20896
+ changed: true,
20897
+ backupPath,
20898
+ skipped: false
20899
+ };
20900
+ }
20901
+ async function readHermesConfigDocument2(configPath) {
20902
+ const existingRaw = await readFile14(configPath, "utf8").catch(
20903
+ (error) => {
20904
+ if (isNodeError16(error, "ENOENT")) {
20905
+ return null;
20906
+ }
20907
+ throw error;
20908
+ }
20909
+ );
20910
+ const document = existingRaw ? YAML3.parseDocument(existingRaw) : new YAML3.Document({});
20911
+ return {
20912
+ document,
20913
+ config: toRecord15(document.toJSON()),
20914
+ existingRaw
20915
+ };
20916
+ }
20917
+ async function writeHermesConfigDocument2(input) {
20918
+ const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
20919
+ if (backupPath) {
20920
+ await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
20921
+ metadataSourcePath: input.configPath
20922
+ });
20923
+ }
20924
+ input.document.contents = input.document.createNode(input.config);
20925
+ await atomicWriteFilePreservingMetadata(
20926
+ input.configPath,
20927
+ input.document.toString()
20928
+ );
20929
+ return backupPath;
20930
+ }
20931
+ function appendExternalDir(current, externalDir, hermesHome) {
20932
+ const entries = readExternalDirEntries(current);
20933
+ const seen = new Set(
20934
+ entries.map((entry) => resolveExternalDirEntry(entry, hermesHome))
20935
+ );
20936
+ const normalizedExternalDir = path21.resolve(externalDir);
20937
+ return seen.has(normalizedExternalDir) ? entries : [...entries, normalizedExternalDir];
20938
+ }
20939
+ function externalDirsInclude(current, externalDir, hermesHome) {
20940
+ const normalizedExternalDir = path21.resolve(externalDir);
20941
+ return readExternalDirEntries(current).some(
20942
+ (entry) => resolveExternalDirEntry(entry, hermesHome) === normalizedExternalDir
20943
+ );
20944
+ }
20945
+ function readExternalDirEntries(value) {
20946
+ const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
20947
+ return raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
20948
+ }
20949
+ function resolveExternalDirEntry(entry, hermesHome) {
20950
+ const expanded = expandHome(expandEnvVars(entry));
20951
+ return path21.resolve(path21.isAbsolute(expanded) ? expanded : path21.join(hermesHome, expanded));
20952
+ }
20953
+ function expandHome(value) {
20954
+ if (value === "~") {
20955
+ return os4.homedir();
20956
+ }
20957
+ if (value.startsWith(`~${path21.sep}`) || value.startsWith("~/")) {
20958
+ return path21.join(os4.homedir(), value.slice(2));
20959
+ }
20960
+ return value;
20961
+ }
20962
+ function expandEnvVars(value) {
20963
+ return value.replace(
20964
+ /\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/gu,
20965
+ (_match, braced, bare) => process.env[braced ?? bare ?? ""] ?? ""
20966
+ );
20967
+ }
20968
+ async function pathIsDirectory(filePath) {
20969
+ return stat14(filePath).then((value) => value.isDirectory()).catch((error) => {
20970
+ if (isNodeError16(error, "ENOENT")) {
20971
+ return false;
20972
+ }
20973
+ throw error;
20974
+ });
20975
+ }
20976
+ function ensureRecord2(target, key) {
20977
+ const existing = target[key];
20978
+ if (isRecord3(existing)) {
20979
+ return existing;
20980
+ }
20981
+ const next = {};
20982
+ target[key] = next;
20983
+ return next;
20984
+ }
20985
+ function toRecord15(value) {
20986
+ return isRecord3(value) ? value : {};
20987
+ }
20988
+ function isRecord3(value) {
20989
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20990
+ }
20991
+ function isNodeError16(error, code) {
20992
+ return error instanceof Error && "code" in error && error.code === code;
20993
+ }
20994
+
20995
+ // src/hermes/profile-creation.ts
20996
+ var PROFILE_CREATE_LOG_FILE = "profile-create.log";
20997
+ var PROFILE_CREATE_LOG_MAX_FILES = 3;
20998
+ var MAX_PROFILE_CREATE_LOG_LINES = 260;
20999
+ var MAX_OUTPUT_LINE_LENGTH = 1200;
21000
+ var PROFILE_NAME_PATTERN5 = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
21001
+ var ALL_COPY_SCOPES = [
21002
+ "models",
21003
+ "skills",
21004
+ "tool_permissions",
21005
+ "approval_policy"
21006
+ ];
21007
+ var PROFILE_CREATION_EVENTS = new EventEmitter2();
21008
+ var runningProfileCreation = null;
21009
+ async function startHermesProfileCreation(input, options) {
21010
+ const current = await readHermesProfileCreationStatus(options.paths);
21011
+ if (runningProfileCreation || current.state === "running") {
21012
+ return current;
21013
+ }
21014
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
21015
+ const normalized = await normalizeProfileCreationInput(input, options.paths);
21016
+ const profileName = normalized.name ?? await generateProfileName(normalized.displayName, options.paths);
21017
+ const sourceProfile = normalized.copyFrom;
21018
+ const copyScopes = normalized.copyScopes;
21019
+ const jobId = `profile_create_${now().getTime().toString(36)}`;
21020
+ await clearProfileCreationLogFiles(options.paths);
21021
+ const writer = createRotatingTextLogWriter({
21022
+ paths: options.paths,
21023
+ fileName: PROFILE_CREATE_LOG_FILE,
21024
+ maxFileBytes: 512 * 1024,
21025
+ maxFiles: PROFILE_CREATE_LOG_MAX_FILES
21026
+ });
21027
+ const startedAt = now().toISOString();
21028
+ const started = {
21029
+ state: "running",
21030
+ job_id: jobId,
21031
+ profile_name: profileName,
21032
+ source_profile: sourceProfile,
21033
+ copied_scopes: [],
21034
+ profile: null,
21035
+ pid: null,
21036
+ started_at: startedAt,
21037
+ finished_at: null,
21038
+ exit_code: null,
21039
+ signal: null,
21040
+ error: null
21041
+ };
21042
+ await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
21043
+ await writeProfileCreationState(options.paths, started);
21044
+ await writer.write(`
21045
+ === profile creation started ${startedAt} ===
21046
+ `);
21047
+ await writer.write(`Profile ID: ${profileName}
21048
+ `);
21049
+ if (sourceProfile && copyScopes.length > 0) {
21050
+ await writer.write(
21051
+ `Copy from: ${sourceProfile} (${copyScopes.join(", ")})
21052
+ `
21053
+ );
21054
+ } else {
21055
+ await writer.write("Copy from: none\n");
21056
+ }
21057
+ const commandArgs = ["profile", "create", profileName, "--no-alias"];
21058
+ await writer.write(`$ ${resolveHermesBin()} ${commandArgs.join(" ")}
21059
+ `);
21060
+ await emitProfileCreationStatus(options.paths);
21061
+ const child = spawn2(resolveHermesBin(), commandArgs, {
21062
+ stdio: ["ignore", "pipe", "pipe"],
21063
+ env: { ...process.env, HERMES_NONINTERACTIVE: "1" },
21064
+ windowsHide: true,
21065
+ detached: false
21066
+ });
21067
+ started.pid = child.pid ?? null;
21068
+ await writeProfileCreationState(options.paths, started);
20040
21069
  await emitProfileCreationStatus(options.paths);
20041
21070
  const appendChunk = async (chunk) => {
20042
21071
  await writer.write(chunk);
@@ -20300,6 +21329,11 @@ async function applyProfileCreationPostSteps(input) {
20300
21329
  await input.writer.write("Ensuring Hermes API Server config...\n");
20301
21330
  await ensureHermesApiServerKey(input.profileName);
20302
21331
  await getHermesProfileStatus(input.profileName, input.paths);
21332
+ await input.writer.write("Ensuring Hermes Link skill...\n");
21333
+ await ensureHermesLinkSkillInstalledBestEffort({
21334
+ paths: input.paths,
21335
+ source: "profile_creation"
21336
+ });
20303
21337
  if (input.displayName || input.description || input.avatarType === "url" || input.avatarUrl) {
20304
21338
  await input.writer.write("Saving Profile display metadata...\n");
20305
21339
  await updateProfileMetadata(input.paths, {
@@ -20352,23 +21386,23 @@ function copyModelConfig(source, target) {
20352
21386
  copied[key] = cloneJson(source[key]);
20353
21387
  }
20354
21388
  }
20355
- const sourceAuxiliary = toRecord15(source.auxiliary);
21389
+ const sourceAuxiliary = toRecord16(source.auxiliary);
20356
21390
  if (Object.prototype.hasOwnProperty.call(sourceAuxiliary, "compression")) {
20357
- const targetAuxiliary = ensureRecord2(target, "auxiliary");
21391
+ const targetAuxiliary = ensureRecord3(target, "auxiliary");
20358
21392
  targetAuxiliary.compression = cloneJson(sourceAuxiliary.compression);
20359
21393
  copied.auxiliary = { compression: cloneJson(sourceAuxiliary.compression) };
20360
21394
  }
20361
21395
  return copied;
20362
21396
  }
20363
21397
  function copyToolPermissionsConfig(source, target) {
20364
- const sourcePlatformToolsets = toRecord15(source.platform_toolsets);
21398
+ const sourcePlatformToolsets = toRecord16(source.platform_toolsets);
20365
21399
  if (Object.prototype.hasOwnProperty.call(sourcePlatformToolsets, "api_server")) {
20366
- const targetPlatformToolsets = ensureRecord2(target, "platform_toolsets");
21400
+ const targetPlatformToolsets = ensureRecord3(target, "platform_toolsets");
20367
21401
  targetPlatformToolsets.api_server = cloneJson(sourcePlatformToolsets.api_server);
20368
21402
  }
20369
- const sourceStt = toRecord15(source.stt);
21403
+ const sourceStt = toRecord16(source.stt);
20370
21404
  if (Object.prototype.hasOwnProperty.call(sourceStt, "enabled")) {
20371
- const targetStt = ensureRecord2(target, "stt");
21405
+ const targetStt = ensureRecord3(target, "stt");
20372
21406
  targetStt.enabled = cloneJson(sourceStt.enabled);
20373
21407
  }
20374
21408
  copyProperty(source, target, "command_allowlist");
@@ -20413,9 +21447,9 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
20413
21447
  return keys;
20414
21448
  }
20415
21449
  async function writeEnvValues(profileName, values) {
20416
- const envPath = path21.join(resolveHermesProfileDir(profileName), ".env");
20417
- const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
20418
- if (isNodeError16(error, "ENOENT")) {
21450
+ const envPath = path22.join(resolveHermesProfileDir(profileName), ".env");
21451
+ const existingRaw = await readFile15(envPath, "utf8").catch((error) => {
21452
+ if (isNodeError17(error, "ENOENT")) {
20419
21453
  return "";
20420
21454
  }
20421
21455
  throw error;
@@ -20450,8 +21484,8 @@ async function writeEnvValues(profileName, values) {
20450
21484
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
20451
21485
  }
20452
21486
  async function copySkills(sourceProfile, targetProfile) {
20453
- const sourceSkills = path21.join(resolveHermesProfileDir(sourceProfile), "skills");
20454
- const targetSkills = path21.join(resolveHermesProfileDir(targetProfile), "skills");
21487
+ const sourceSkills = path22.join(resolveHermesProfileDir(sourceProfile), "skills");
21488
+ const targetSkills = path22.join(resolveHermesProfileDir(targetProfile), "skills");
20455
21489
  if (!await pathExists2(sourceSkills)) {
20456
21490
  return;
20457
21491
  }
@@ -20474,16 +21508,16 @@ function copyProperty(source, target, key) {
20474
21508
  }
20475
21509
  }
20476
21510
  async function readYamlConfig(configPath) {
20477
- const existingRaw = await readFile14(configPath, "utf8").catch(
21511
+ const existingRaw = await readFile15(configPath, "utf8").catch(
20478
21512
  (error) => {
20479
- if (isNodeError16(error, "ENOENT")) {
21513
+ if (isNodeError17(error, "ENOENT")) {
20480
21514
  return null;
20481
21515
  }
20482
21516
  throw error;
20483
21517
  }
20484
21518
  );
20485
21519
  return {
20486
- config: toRecord15(existingRaw ? YAML3.parse(existingRaw) : {}),
21520
+ config: toRecord16(existingRaw ? YAML4.parse(existingRaw) : {}),
20487
21521
  existingRaw
20488
21522
  };
20489
21523
  }
@@ -20495,7 +21529,7 @@ async function writeYamlConfig(configPath, input) {
20495
21529
  { metadataSourcePath: configPath }
20496
21530
  );
20497
21531
  }
20498
- const document = new YAML3.Document(input.config);
21532
+ const document = new YAML4.Document(input.config);
20499
21533
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
20500
21534
  }
20501
21535
  async function failProfileCreation(input) {
@@ -20537,7 +21571,7 @@ async function writeProfileCreationState(paths, state) {
20537
21571
  await writeJsonFile(profileCreationStatePath(paths), state);
20538
21572
  }
20539
21573
  async function readProfileCreationLogLines(paths) {
20540
- const raw = await readFile14(profileCreationLogPath(paths), "utf8").catch(() => "");
21574
+ const raw = await readFile15(profileCreationLogPath(paths), "utf8").catch(() => "");
20541
21575
  if (!raw.trim()) {
20542
21576
  return [];
20543
21577
  }
@@ -20546,10 +21580,10 @@ async function readProfileCreationLogLines(paths) {
20546
21580
  );
20547
21581
  }
20548
21582
  function profileCreationStatePath(paths) {
20549
- return path21.join(paths.runDir, "profile-create-state.json");
21583
+ return path22.join(paths.runDir, "profile-create-state.json");
20550
21584
  }
20551
21585
  function profileCreationLogPath(paths) {
20552
- return path21.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
21586
+ return path22.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
20553
21587
  }
20554
21588
  async function clearProfileCreationLogFiles(paths) {
20555
21589
  const primary = profileCreationLogPath(paths);
@@ -20562,8 +21596,8 @@ async function clearProfileCreationLogFiles(paths) {
20562
21596
  ]);
20563
21597
  }
20564
21598
  async function pathExists2(targetPath) {
20565
- return await stat14(targetPath).then(() => true).catch((error) => {
20566
- if (isNodeError16(error, "ENOENT")) {
21599
+ return await stat15(targetPath).then(() => true).catch((error) => {
21600
+ if (isNodeError17(error, "ENOENT")) {
20567
21601
  return false;
20568
21602
  }
20569
21603
  throw error;
@@ -20586,7 +21620,7 @@ function isProcessAlive(pid) {
20586
21620
  return false;
20587
21621
  }
20588
21622
  }
20589
- function ensureRecord2(target, key) {
21623
+ function ensureRecord3(target, key) {
20590
21624
  const value = target[key];
20591
21625
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
20592
21626
  return value;
@@ -20595,7 +21629,7 @@ function ensureRecord2(target, key) {
20595
21629
  target[key] = next;
20596
21630
  return next;
20597
21631
  }
20598
- function toRecord15(value) {
21632
+ function toRecord16(value) {
20599
21633
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
20600
21634
  }
20601
21635
  function cloneJson(value) {
@@ -20610,7 +21644,7 @@ function formatEnvValue2(value) {
20610
21644
  function escapeRegExp3(value) {
20611
21645
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
20612
21646
  }
20613
- function isNodeError16(error, code) {
21647
+ function isNodeError17(error, code) {
20614
21648
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
20615
21649
  }
20616
21650
 
@@ -20820,11 +21854,11 @@ function toProfileToolConfigHttpError(error) {
20820
21854
  import {
20821
21855
  access as access3,
20822
21856
  readdir as readdir10,
20823
- readFile as readFile15,
20824
- stat as stat15
21857
+ readFile as readFile16,
21858
+ stat as stat16
20825
21859
  } from "fs/promises";
20826
- import path22 from "path";
20827
- import YAML4 from "yaml";
21860
+ import path23 from "path";
21861
+ import YAML5 from "yaml";
20828
21862
  var ENTRY_DELIMITER = "\n\xA7\n";
20829
21863
  var DEFAULT_MEMORY_LIMIT = 2200;
20830
21864
  var DEFAULT_USER_LIMIT = 1375;
@@ -21145,7 +22179,7 @@ async function saveProviderSettings(profileName, provider, patch) {
21145
22179
  });
21146
22180
  await patchJsonProviderConfig(
21147
22181
  profileName,
21148
- path22.join("hindsight", "config.json"),
22182
+ path23.join("hindsight", "config.json"),
21149
22183
  {
21150
22184
  mode: patch.mode,
21151
22185
  api_url: patch.apiUrl,
@@ -21202,7 +22236,7 @@ async function patchCustomProviderConfig(profileName, provider, patch) {
21202
22236
  "\u81EA\u5B9A\u4E49 memory provider \u914D\u7F6E\u5FC5\u987B\u662F\u6709\u6548\u7684 JSON object\u3002"
21203
22237
  );
21204
22238
  }
21205
- const config = toRecord16(parsed);
22239
+ const config = toRecord17(parsed);
21206
22240
  if (Object.keys(config).length === 0 && parsed !== null) {
21207
22241
  throw new HermesMemoryError(
21208
22242
  "memory_provider_config_invalid",
@@ -21293,17 +22327,17 @@ function normalizeCustomProviderId(provider) {
21293
22327
  }
21294
22328
  async function patchHermesMemoryProvider(profileName, provider) {
21295
22329
  const configPath = resolveHermesConfigPath(profileName);
21296
- const existingRaw = await readFile15(configPath, "utf8").catch(
22330
+ const existingRaw = await readFile16(configPath, "utf8").catch(
21297
22331
  (error) => {
21298
- if (isNodeError17(error, "ENOENT")) {
22332
+ if (isNodeError18(error, "ENOENT")) {
21299
22333
  return null;
21300
22334
  }
21301
22335
  throw error;
21302
22336
  }
21303
22337
  );
21304
- const document = existingRaw ? YAML4.parseDocument(existingRaw) : new YAML4.Document({});
21305
- const config = toRecord16(document.toJSON());
21306
- const memory = toRecord16(config.memory);
22338
+ const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
22339
+ const config = toRecord17(document.toJSON());
22340
+ const memory = toRecord17(config.memory);
21307
22341
  memory.provider = provider === "built-in" ? "" : provider;
21308
22342
  config.memory = memory;
21309
22343
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
@@ -21316,13 +22350,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
21316
22350
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
21317
22351
  }
21318
22352
  function resolveMemoryDir(profileName) {
21319
- return path22.join(resolveHermesProfileDir(profileName), "memories");
22353
+ return path23.join(resolveHermesProfileDir(profileName), "memories");
21320
22354
  }
21321
22355
  async function readMemoryStore(profileName, target, limits) {
21322
22356
  const filePath = memoryFilePath(profileName, target);
21323
22357
  const entries = await readMemoryEntries(filePath);
21324
- const fileStat = await stat15(filePath).catch((error) => {
21325
- if (isNodeError17(error, "ENOENT")) {
22358
+ const fileStat = await stat16(filePath).catch((error) => {
22359
+ if (isNodeError18(error, "ENOENT")) {
21326
22360
  return null;
21327
22361
  }
21328
22362
  throw error;
@@ -21350,8 +22384,8 @@ async function readMemoryStore(profileName, target, limits) {
21350
22384
  };
21351
22385
  }
21352
22386
  async function readMemoryEntries(filePath) {
21353
- const raw = await readFile15(filePath, "utf8").catch((error) => {
21354
- if (isNodeError17(error, "ENOENT")) {
22387
+ const raw = await readFile16(filePath, "utf8").catch((error) => {
22388
+ if (isNodeError18(error, "ENOENT")) {
21355
22389
  return "";
21356
22390
  }
21357
22391
  throw error;
@@ -21377,7 +22411,7 @@ async function writeMemoryEntries(profileName, target, entries) {
21377
22411
  );
21378
22412
  }
21379
22413
  function memoryFilePath(profileName, target) {
21380
- return path22.join(
22414
+ return path23.join(
21381
22415
  resolveMemoryDir(profileName),
21382
22416
  target === "user" ? "USER.md" : "MEMORY.md"
21383
22417
  );
@@ -21410,7 +22444,7 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
21410
22444
  }
21411
22445
  return Promise.all(
21412
22446
  [...descriptors.values()].map(async (descriptor) => {
21413
- const installed = await isUserMemoryProviderInstalled(
22447
+ const installed2 = await isUserMemoryProviderInstalled(
21414
22448
  profileName,
21415
22449
  descriptor.id
21416
22450
  );
@@ -21420,8 +22454,8 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
21420
22454
  description: descriptor.description,
21421
22455
  active: descriptor.id === activeProviderId,
21422
22456
  configurable: true,
21423
- configured: installed,
21424
- configurationIssue: installed ? null : "\u6CA1\u6709\u5728\u5F53\u524D HERMES_HOME/plugins/ \u4E0B\u53D1\u73B0\u8FD9\u4E2A\u81EA\u5B9A\u4E49 memory provider\u3002",
22457
+ configured: installed2,
22458
+ configurationIssue: installed2 ? null : "\u6CA1\u6709\u5728\u5F53\u524D HERMES_HOME/plugins/ \u4E0B\u53D1\u73B0\u8FD9\u4E2A\u81EA\u5B9A\u4E49 memory provider\u3002",
21425
22459
  providerConfigPath: customProviderConfigPath(profileName, descriptor.id),
21426
22460
  settings: await readCustomProviderSettings(profileName, descriptor.id)
21427
22461
  };
@@ -21437,7 +22471,7 @@ async function readCustomProviderSetupSummary(profileName) {
21437
22471
  configurable: true,
21438
22472
  configured: true,
21439
22473
  configurationIssue: null,
21440
- providerConfigPath: path22.join(
22474
+ providerConfigPath: path23.join(
21441
22475
  resolveHermesProfileDir(profileName),
21442
22476
  "<provider>.json"
21443
22477
  ),
@@ -21766,8 +22800,8 @@ async function readProviderSettings(profileName, provider) {
21766
22800
  memoryProviderConfigPath(profileName, provider) ?? ""
21767
22801
  );
21768
22802
  const env = await readHermesMemoryEnv(profileName);
21769
- const banks = toRecord16(config.banks);
21770
- const hermesBank = toRecord16(banks.hermes);
22803
+ const banks = toRecord17(config.banks);
22804
+ const hermesBank = toRecord17(banks.hermes);
21771
22805
  const mode = normalizeHindsightMode(config.mode);
21772
22806
  return [
21773
22807
  selectSetting("mode", "\u8FDE\u63A5\u6A21\u5F0F", mode, [
@@ -21831,7 +22865,7 @@ async function readProviderSettings(profileName, provider) {
21831
22865
  stringSetting(
21832
22866
  "dbPath",
21833
22867
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
21834
- config.db_path ?? path22.join(resolveHermesProfileDir(profileName), "memory_store.db")
22868
+ config.db_path ?? path23.join(resolveHermesProfileDir(profileName), "memory_store.db")
21835
22869
  ),
21836
22870
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
21837
22871
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -21867,7 +22901,7 @@ async function readProviderSettings(profileName, provider) {
21867
22901
  stringSetting(
21868
22902
  "workingDirectory",
21869
22903
  "\u5DE5\u4F5C\u76EE\u5F55",
21870
- path22.join(resolveHermesProfileDir(profileName), "byterover"),
22904
+ path23.join(resolveHermesProfileDir(profileName), "byterover"),
21871
22905
  false
21872
22906
  )
21873
22907
  ];
@@ -21876,16 +22910,16 @@ async function readProviderSettings(profileName, provider) {
21876
22910
  }
21877
22911
  function memoryProviderConfigPath(profileName, provider) {
21878
22912
  if (provider === "honcho") {
21879
- return path22.join(resolveHermesProfileDir(profileName), "honcho.json");
22913
+ return path23.join(resolveHermesProfileDir(profileName), "honcho.json");
21880
22914
  }
21881
22915
  if (provider === "mem0") {
21882
- return path22.join(resolveHermesProfileDir(profileName), "mem0.json");
22916
+ return path23.join(resolveHermesProfileDir(profileName), "mem0.json");
21883
22917
  }
21884
22918
  if (provider === "supermemory") {
21885
- return path22.join(resolveHermesProfileDir(profileName), "supermemory.json");
22919
+ return path23.join(resolveHermesProfileDir(profileName), "supermemory.json");
21886
22920
  }
21887
22921
  if (provider === "hindsight") {
21888
- return path22.join(
22922
+ return path23.join(
21889
22923
  resolveHermesProfileDir(profileName),
21890
22924
  "hindsight",
21891
22925
  "config.json"
@@ -21894,21 +22928,21 @@ function memoryProviderConfigPath(profileName, provider) {
21894
22928
  return null;
21895
22929
  }
21896
22930
  function customProviderConfigPath(profileName, provider) {
21897
- return path22.join(
22931
+ return path23.join(
21898
22932
  resolveHermesProfileDir(profileName),
21899
22933
  `${normalizeCustomProviderId(provider)}.json`
21900
22934
  );
21901
22935
  }
21902
22936
  function customProviderRegistryPath(profileName) {
21903
- return path22.join(
22937
+ return path23.join(
21904
22938
  resolveHermesProfileDir(profileName),
21905
22939
  CUSTOM_PROVIDER_REGISTRY_FILE
21906
22940
  );
21907
22941
  }
21908
22942
  async function readCustomProviderRegistry(profileName) {
21909
- const raw = await readFile15(customProviderRegistryPath(profileName), "utf8").catch(
22943
+ const raw = await readFile16(customProviderRegistryPath(profileName), "utf8").catch(
21910
22944
  (error) => {
21911
- if (isNodeError17(error, "ENOENT")) {
22945
+ if (isNodeError18(error, "ENOENT")) {
21912
22946
  return "";
21913
22947
  }
21914
22948
  throw error;
@@ -21919,13 +22953,13 @@ async function readCustomProviderRegistry(profileName) {
21919
22953
  }
21920
22954
  try {
21921
22955
  const parsed = JSON.parse(raw);
21922
- const providers = Array.isArray(parsed) ? parsed : Array.isArray(toRecord16(parsed).providers) ? toRecord16(parsed).providers : [];
22956
+ const providers = Array.isArray(parsed) ? parsed : Array.isArray(toRecord17(parsed).providers) ? toRecord17(parsed).providers : [];
21923
22957
  return providers.map((item) => {
21924
22958
  if (typeof item === "string") {
21925
22959
  const id2 = normalizeCustomProviderId(item);
21926
22960
  return { id: id2, label: id2, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" };
21927
22961
  }
21928
- const record = toRecord16(item);
22962
+ const record = toRecord17(item);
21929
22963
  const id = normalizeCustomProviderId(readString17(record.id) ?? "");
21930
22964
  return {
21931
22965
  id,
@@ -21952,10 +22986,10 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
21952
22986
  );
21953
22987
  }
21954
22988
  async function discoverUserMemoryProviderDescriptors(profileName) {
21955
- const pluginsDir = path22.join(resolveHermesProfileDir(profileName), "plugins");
22989
+ const pluginsDir = path23.join(resolveHermesProfileDir(profileName), "plugins");
21956
22990
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
21957
22991
  (error) => {
21958
- if (isNodeError17(error, "ENOENT")) {
22992
+ if (isNodeError18(error, "ENOENT")) {
21959
22993
  return [];
21960
22994
  }
21961
22995
  throw error;
@@ -21972,7 +23006,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
21972
23006
  } catch {
21973
23007
  continue;
21974
23008
  }
21975
- const providerDir = path22.join(pluginsDir, entry.name);
23009
+ const providerDir = path23.join(pluginsDir, entry.name);
21976
23010
  if (!await isMemoryProviderPluginDir(providerDir)) {
21977
23011
  continue;
21978
23012
  }
@@ -21986,7 +23020,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
21986
23020
  return descriptors;
21987
23021
  }
21988
23022
  async function isUserMemoryProviderInstalled(profileName, provider) {
21989
- const providerDir = path22.join(
23023
+ const providerDir = path23.join(
21990
23024
  resolveHermesProfileDir(profileName),
21991
23025
  "plugins",
21992
23026
  normalizeCustomProviderId(provider)
@@ -21994,9 +23028,9 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
21994
23028
  return isMemoryProviderPluginDir(providerDir);
21995
23029
  }
21996
23030
  async function isMemoryProviderPluginDir(providerDir) {
21997
- const source = await readFile15(path22.join(providerDir, "__init__.py"), "utf8").catch(
23031
+ const source = await readFile16(path23.join(providerDir, "__init__.py"), "utf8").catch(
21998
23032
  (error) => {
21999
- if (isNodeError17(error, "ENOENT")) {
23033
+ if (isNodeError18(error, "ENOENT")) {
22000
23034
  return "";
22001
23035
  }
22002
23036
  throw error;
@@ -22006,22 +23040,22 @@ async function isMemoryProviderPluginDir(providerDir) {
22006
23040
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
22007
23041
  }
22008
23042
  async function readPluginMetadata(providerDir) {
22009
- const raw = await readFile15(path22.join(providerDir, "plugin.yaml"), "utf8").catch(
23043
+ const raw = await readFile16(path23.join(providerDir, "plugin.yaml"), "utf8").catch(
22010
23044
  (error) => {
22011
- if (isNodeError17(error, "ENOENT")) {
23045
+ if (isNodeError18(error, "ENOENT")) {
22012
23046
  return "";
22013
23047
  }
22014
23048
  throw error;
22015
23049
  }
22016
23050
  );
22017
- return raw ? toRecord16(YAML4.parse(raw)) : {};
23051
+ return raw ? toRecord17(YAML5.parse(raw)) : {};
22018
23052
  }
22019
23053
  async function resolveByteRoverCli() {
22020
23054
  const candidates = [
22021
- ...(process.env.PATH ?? "").split(path22.delimiter).filter(Boolean).map((dir) => path22.join(dir, "brv")),
22022
- path22.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
23055
+ ...(process.env.PATH ?? "").split(path23.delimiter).filter(Boolean).map((dir) => path23.join(dir, "brv")),
23056
+ path23.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
22023
23057
  "/usr/local/bin/brv",
22024
- path22.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
23058
+ path23.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
22025
23059
  ].filter(Boolean);
22026
23060
  for (const candidate of candidates) {
22027
23061
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -22032,32 +23066,32 @@ async function resolveByteRoverCli() {
22032
23066
  return null;
22033
23067
  }
22034
23068
  async function readHolographicProviderConfig(profileName) {
22035
- const raw = await readFile15(resolveHermesConfigPath(profileName), "utf8").catch(
23069
+ const raw = await readFile16(resolveHermesConfigPath(profileName), "utf8").catch(
22036
23070
  (error) => {
22037
- if (isNodeError17(error, "ENOENT")) {
23071
+ if (isNodeError18(error, "ENOENT")) {
22038
23072
  return "";
22039
23073
  }
22040
23074
  throw error;
22041
23075
  }
22042
23076
  );
22043
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
22044
- const plugins = toRecord16(config.plugins);
22045
- return toRecord16(plugins["hermes-memory-store"]);
23077
+ const config = raw ? toRecord17(YAML5.parse(raw)) : {};
23078
+ const plugins = toRecord17(config.plugins);
23079
+ return toRecord17(plugins["hermes-memory-store"]);
22046
23080
  }
22047
23081
  async function patchHolographicProviderConfig(profileName, patch) {
22048
23082
  const configPath = resolveHermesConfigPath(profileName);
22049
- const existingRaw = await readFile15(configPath, "utf8").catch(
23083
+ const existingRaw = await readFile16(configPath, "utf8").catch(
22050
23084
  (error) => {
22051
- if (isNodeError17(error, "ENOENT")) {
23085
+ if (isNodeError18(error, "ENOENT")) {
22052
23086
  return null;
22053
23087
  }
22054
23088
  throw error;
22055
23089
  }
22056
23090
  );
22057
- const document = existingRaw ? YAML4.parseDocument(existingRaw) : new YAML4.Document({});
22058
- const config = toRecord16(document.toJSON());
22059
- const plugins = toRecord16(config.plugins);
22060
- const memoryStore = toRecord16(plugins["hermes-memory-store"]);
23091
+ const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
23092
+ const config = toRecord17(document.toJSON());
23093
+ const plugins = toRecord17(config.plugins);
23094
+ const memoryStore = toRecord17(plugins["hermes-memory-store"]);
22061
23095
  for (const [key, value] of Object.entries(patch)) {
22062
23096
  if (value !== void 0) {
22063
23097
  memoryStore[key] = value;
@@ -22082,9 +23116,9 @@ async function patchHermesMemoryEnv(profileName, patch) {
22082
23116
  if (entries.length === 0) {
22083
23117
  return;
22084
23118
  }
22085
- const envPath = path22.join(resolveHermesProfileDir(profileName), ".env");
22086
- const existingRaw = await readFile15(envPath, "utf8").catch((error) => {
22087
- if (isNodeError17(error, "ENOENT")) {
23119
+ const envPath = path23.join(resolveHermesProfileDir(profileName), ".env");
23120
+ const existingRaw = await readFile16(envPath, "utf8").catch((error) => {
23121
+ if (isNodeError18(error, "ENOENT")) {
22088
23122
  return "";
22089
23123
  }
22090
23124
  throw error;
@@ -22205,7 +23239,7 @@ function joinHindsightUrl(baseUrl, pathName) {
22205
23239
  }
22206
23240
  function parseJsonObject2(text) {
22207
23241
  try {
22208
- return toRecord16(JSON.parse(text));
23242
+ return toRecord17(JSON.parse(text));
22209
23243
  } catch {
22210
23244
  return {};
22211
23245
  }
@@ -22235,17 +23269,17 @@ function summarizeHindsightProbe(pathName, json) {
22235
23269
  return bankId ? `bank ${bankId} reachable` : "bank config reachable";
22236
23270
  }
22237
23271
  async function readActiveMemoryProvider(profileName) {
22238
- const raw = await readFile15(
23272
+ const raw = await readFile16(
22239
23273
  resolveHermesConfigPath(profileName),
22240
23274
  "utf8"
22241
23275
  ).catch((error) => {
22242
- if (isNodeError17(error, "ENOENT")) {
23276
+ if (isNodeError18(error, "ENOENT")) {
22243
23277
  return "";
22244
23278
  }
22245
23279
  throw error;
22246
23280
  });
22247
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
22248
- const memory = toRecord16(config.memory);
23281
+ const config = raw ? toRecord17(YAML5.parse(raw)) : {};
23282
+ const memory = toRecord17(config.memory);
22249
23283
  const provider = readString17(memory.provider);
22250
23284
  if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
22251
23285
  return null;
@@ -22253,7 +23287,7 @@ async function readActiveMemoryProvider(profileName) {
22253
23287
  return provider;
22254
23288
  }
22255
23289
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
22256
- const configPath = path22.join(
23290
+ const configPath = path23.join(
22257
23291
  resolveHermesProfileDir(profileName),
22258
23292
  relativePath
22259
23293
  );
@@ -22271,18 +23305,18 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
22271
23305
  );
22272
23306
  }
22273
23307
  async function readJsonObject(filePath) {
22274
- const raw = await readFile15(filePath, "utf8").catch((error) => {
22275
- if (isNodeError17(error, "ENOENT")) {
23308
+ const raw = await readFile16(filePath, "utf8").catch((error) => {
23309
+ if (isNodeError18(error, "ENOENT")) {
22276
23310
  return "{}";
22277
23311
  }
22278
23312
  throw error;
22279
23313
  });
22280
23314
  try {
22281
- return toRecord16(JSON.parse(raw || "{}"));
23315
+ return toRecord17(JSON.parse(raw || "{}"));
22282
23316
  } catch {
22283
23317
  throw new HermesMemoryError(
22284
23318
  "memory_provider_config_invalid",
22285
- `${path22.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
23319
+ `${path23.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
22286
23320
  );
22287
23321
  }
22288
23322
  }
@@ -22332,17 +23366,17 @@ function selectSetting(key, label, value, options, editable = true) {
22332
23366
  return { key, label, value: stringValue, editable, kind: "select", options };
22333
23367
  }
22334
23368
  async function readMemoryLimits(profileName) {
22335
- const raw = await readFile15(
23369
+ const raw = await readFile16(
22336
23370
  resolveHermesConfigPath(profileName),
22337
23371
  "utf8"
22338
23372
  ).catch((error) => {
22339
- if (isNodeError17(error, "ENOENT")) {
23373
+ if (isNodeError18(error, "ENOENT")) {
22340
23374
  return "";
22341
23375
  }
22342
23376
  throw error;
22343
23377
  });
22344
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
22345
- const memory = toRecord16(config.memory);
23378
+ const config = raw ? toRecord17(YAML5.parse(raw)) : {};
23379
+ const memory = toRecord17(config.memory);
22346
23380
  return {
22347
23381
  memory: readPositiveInteger3(memory.memory_char_limit) ?? DEFAULT_MEMORY_LIMIT,
22348
23382
  user: readPositiveInteger3(memory.user_char_limit) ?? DEFAULT_USER_LIMIT
@@ -22398,7 +23432,7 @@ function hashString(value) {
22398
23432
  }
22399
23433
  return hash.toString(16);
22400
23434
  }
22401
- function toRecord16(value) {
23435
+ function toRecord17(value) {
22402
23436
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
22403
23437
  }
22404
23438
  function readString17(value) {
@@ -22429,7 +23463,7 @@ function formatEnvValue3(value) {
22429
23463
  function escapeRegExp4(value) {
22430
23464
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
22431
23465
  }
22432
- function isNodeError17(error, code) {
23466
+ function isNodeError18(error, code) {
22433
23467
  return error instanceof Error && "code" in error && error.code === code;
22434
23468
  }
22435
23469
 
@@ -22870,9 +23904,9 @@ function toMemoryHttpError(error) {
22870
23904
  }
22871
23905
 
22872
23906
  // src/hermes/skills.ts
22873
- import { readFile as readFile16, readdir as readdir11 } from "fs/promises";
22874
- import path23 from "path";
22875
- import YAML5 from "yaml";
23907
+ import { readFile as readFile17, readdir as readdir11 } from "fs/promises";
23908
+ import path24 from "path";
23909
+ import YAML6 from "yaml";
22876
23910
  var HermesSkillNotFoundError = class extends Error {
22877
23911
  constructor(skillName) {
22878
23912
  super(`skill "${skillName}" does not exist`);
@@ -22885,7 +23919,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
22885
23919
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
22886
23920
  const profile = await readExistingProfile(profileName, paths);
22887
23921
  const profileDir = resolveHermesProfileDir(profile.name);
22888
- const skillsRoot = path23.join(profileDir, "skills");
23922
+ const skillsRoot = path24.join(profileDir, "skills");
22889
23923
  const [skillFiles, disabled, provenance] = await Promise.all([
22890
23924
  findSkillFiles(skillsRoot),
22891
23925
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -22921,8 +23955,8 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
22921
23955
  throw new HermesSkillNotFoundError(skillName);
22922
23956
  }
22923
23957
  const configPath = resolveHermesConfigPath(current.profile.name);
22924
- const { document, config, existingRaw } = await readHermesConfigDocument2(configPath);
22925
- const skillsConfig = ensureRecord3(config, "skills");
23958
+ const { document, config, existingRaw } = await readHermesConfigDocument3(configPath);
23959
+ const skillsConfig = ensureRecord4(config, "skills");
22926
23960
  const disabled = new Set(readStringList3(skillsConfig.disabled));
22927
23961
  if (enabled) {
22928
23962
  disabled.delete(target.name);
@@ -22932,7 +23966,7 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
22932
23966
  skillsConfig.disabled = [...disabled].sort(
22933
23967
  (left, right) => left.localeCompare(right)
22934
23968
  );
22935
- const backupPath = await writeHermesConfigDocument2({
23969
+ const backupPath = await writeHermesConfigDocument3({
22936
23970
  configPath,
22937
23971
  document,
22938
23972
  config,
@@ -22964,7 +23998,7 @@ async function findSkillFiles(root) {
22964
23998
  async function collectSkillFiles(directory, results) {
22965
23999
  const entries = await readdir11(directory, { withFileTypes: true }).catch(
22966
24000
  (error) => {
22967
- if (isNodeError18(error, "ENOENT")) {
24001
+ if (isNodeError19(error, "ENOENT")) {
22968
24002
  return [];
22969
24003
  }
22970
24004
  throw error;
@@ -22976,7 +24010,7 @@ async function collectSkillFiles(directory, results) {
22976
24010
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
22977
24011
  continue;
22978
24012
  }
22979
- const entryPath = path23.join(directory, entry.name);
24013
+ const entryPath = path24.join(directory, entry.name);
22980
24014
  if (entry.isDirectory()) {
22981
24015
  await collectSkillFiles(entryPath, results);
22982
24016
  continue;
@@ -22987,9 +24021,9 @@ async function collectSkillFiles(directory, results) {
22987
24021
  }
22988
24022
  }
22989
24023
  async function readSkillMetadata(input) {
22990
- const raw = await readFile16(input.skillFile, "utf8").catch(
24024
+ const raw = await readFile17(input.skillFile, "utf8").catch(
22991
24025
  (error) => {
22992
- if (isNodeError18(error, "ENOENT") || isNodeError18(error, "EACCES")) {
24026
+ if (isNodeError19(error, "ENOENT") || isNodeError19(error, "EACCES")) {
22993
24027
  return null;
22994
24028
  }
22995
24029
  throw error;
@@ -22998,10 +24032,10 @@ async function readSkillMetadata(input) {
22998
24032
  if (raw === null) {
22999
24033
  return null;
23000
24034
  }
23001
- const skillDir = path23.dirname(input.skillFile);
24035
+ const skillDir = path24.dirname(input.skillFile);
23002
24036
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
23003
24037
  const name = normalizeSkillName(
23004
- readString18(frontmatter.name) ?? path23.basename(skillDir)
24038
+ readString18(frontmatter.name) ?? path24.basename(skillDir)
23005
24039
  );
23006
24040
  if (!name) {
23007
24041
  return null;
@@ -23020,7 +24054,7 @@ async function readSkillMetadata(input) {
23020
24054
  enabled: !input.disabled.has(name),
23021
24055
  source: provenance.source,
23022
24056
  trust: provenance.trust,
23023
- relativePath: path23.relative(input.skillsRoot, skillDir)
24057
+ relativePath: path24.relative(input.skillsRoot, skillDir)
23024
24058
  };
23025
24059
  }
23026
24060
  function parseSkillDocument(raw) {
@@ -23033,7 +24067,7 @@ function parseSkillDocument(raw) {
23033
24067
  }
23034
24068
  try {
23035
24069
  return {
23036
- frontmatter: toRecord17(YAML5.parse(match[1] ?? "")),
24070
+ frontmatter: toRecord18(YAML6.parse(match[1] ?? "")),
23037
24071
  body: content.slice(match[0].length)
23038
24072
  };
23039
24073
  } catch {
@@ -23041,8 +24075,8 @@ function parseSkillDocument(raw) {
23041
24075
  }
23042
24076
  }
23043
24077
  function categoryFromPath(skillsRoot, skillFile) {
23044
- const relative = path23.relative(skillsRoot, skillFile);
23045
- const parts = relative.split(path23.sep).filter(Boolean);
24078
+ const relative = path24.relative(skillsRoot, skillFile);
24079
+ const parts = relative.split(path24.sep).filter(Boolean);
23046
24080
  return parts.length >= 3 ? parts[0] : null;
23047
24081
  }
23048
24082
  function firstBodyDescription(body) {
@@ -23065,8 +24099,8 @@ function normalizeDescription(value) {
23065
24099
  return `${description.slice(0, MAX_DESCRIPTION_LENGTH - 3)}...`;
23066
24100
  }
23067
24101
  async function readDisabledSkillNames(configPath) {
23068
- const raw = await readFile16(configPath, "utf8").catch((error) => {
23069
- if (isNodeError18(error, "ENOENT")) {
24102
+ const raw = await readFile17(configPath, "utf8").catch((error) => {
24103
+ if (isNodeError19(error, "ENOENT")) {
23070
24104
  return "";
23071
24105
  }
23072
24106
  throw error;
@@ -23074,8 +24108,8 @@ async function readDisabledSkillNames(configPath) {
23074
24108
  if (!raw.trim()) {
23075
24109
  return /* @__PURE__ */ new Set();
23076
24110
  }
23077
- const config = toRecord17(YAML5.parse(raw));
23078
- const skills = toRecord17(config.skills);
24111
+ const config = toRecord18(YAML6.parse(raw));
24112
+ const skills = toRecord18(config.skills);
23079
24113
  return new Set(readStringList3(skills.disabled));
23080
24114
  }
23081
24115
  async function readSkillProvenance(root) {
@@ -23089,9 +24123,9 @@ async function readSkillProvenance(root) {
23089
24123
  return provenance;
23090
24124
  }
23091
24125
  async function readBundledSkillNames(root) {
23092
- const raw = await readFile16(path23.join(root, ".bundled_manifest"), "utf8").catch(
24126
+ const raw = await readFile17(path24.join(root, ".bundled_manifest"), "utf8").catch(
23093
24127
  (error) => {
23094
- if (isNodeError18(error, "ENOENT")) {
24128
+ if (isNodeError19(error, "ENOENT")) {
23095
24129
  return "";
23096
24130
  }
23097
24131
  throw error;
@@ -23112,9 +24146,9 @@ async function readBundledSkillNames(root) {
23112
24146
  return names;
23113
24147
  }
23114
24148
  async function readHubInstalledSkills(root) {
23115
- const raw = await readFile16(path23.join(root, ".hub", "lock.json"), "utf8").catch(
24149
+ const raw = await readFile17(path24.join(root, ".hub", "lock.json"), "utf8").catch(
23116
24150
  (error) => {
23117
- if (isNodeError18(error, "ENOENT")) {
24151
+ if (isNodeError19(error, "ENOENT")) {
23118
24152
  return "";
23119
24153
  }
23120
24154
  throw error;
@@ -23125,14 +24159,14 @@ async function readHubInstalledSkills(root) {
23125
24159
  }
23126
24160
  let lock;
23127
24161
  try {
23128
- lock = toRecord17(JSON.parse(raw));
24162
+ lock = toRecord18(JSON.parse(raw));
23129
24163
  } catch {
23130
24164
  return /* @__PURE__ */ new Map();
23131
24165
  }
23132
- const installed = toRecord17(lock.installed);
24166
+ const installed2 = toRecord18(lock.installed);
23133
24167
  const result = /* @__PURE__ */ new Map();
23134
- for (const [name, rawEntry] of Object.entries(installed)) {
23135
- const entry = toRecord17(rawEntry);
24168
+ for (const [name, rawEntry] of Object.entries(installed2)) {
24169
+ const entry = toRecord18(rawEntry);
23136
24170
  result.set(normalizeSkillName(name), {
23137
24171
  source: readString18(entry.source) ?? "hub",
23138
24172
  trust: readString18(entry.trust_level) ?? null
@@ -23183,23 +24217,23 @@ function compareCategoryNames(left, right) {
23183
24217
  }
23184
24218
  return left.localeCompare(right);
23185
24219
  }
23186
- async function readHermesConfigDocument2(configPath) {
23187
- const existingRaw = await readFile16(configPath, "utf8").catch(
24220
+ async function readHermesConfigDocument3(configPath) {
24221
+ const existingRaw = await readFile17(configPath, "utf8").catch(
23188
24222
  (error) => {
23189
- if (isNodeError18(error, "ENOENT")) {
24223
+ if (isNodeError19(error, "ENOENT")) {
23190
24224
  return null;
23191
24225
  }
23192
24226
  throw error;
23193
24227
  }
23194
24228
  );
23195
- const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
24229
+ const document = existingRaw ? YAML6.parseDocument(existingRaw) : new YAML6.Document({});
23196
24230
  return {
23197
24231
  document,
23198
- config: toRecord17(document.toJSON()),
24232
+ config: toRecord18(document.toJSON()),
23199
24233
  existingRaw
23200
24234
  };
23201
24235
  }
23202
- async function writeHermesConfigDocument2(input) {
24236
+ async function writeHermesConfigDocument3(input) {
23203
24237
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
23204
24238
  if (backupPath) {
23205
24239
  await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
@@ -23222,18 +24256,18 @@ function readStringList3(value) {
23222
24256
  function readString18(value) {
23223
24257
  return typeof value === "string" && value.trim() ? value.trim() : null;
23224
24258
  }
23225
- function toRecord17(value) {
24259
+ function toRecord18(value) {
23226
24260
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
23227
24261
  }
23228
- function ensureRecord3(target, key) {
23229
- const current = toRecord17(target[key]);
24262
+ function ensureRecord4(target, key) {
24263
+ const current = toRecord18(target[key]);
23230
24264
  if (current === target[key]) {
23231
24265
  return current;
23232
24266
  }
23233
24267
  target[key] = current;
23234
24268
  return current;
23235
24269
  }
23236
- function isNodeError18(error, code) {
24270
+ function isNodeError19(error, code) {
23237
24271
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
23238
24272
  }
23239
24273
 
@@ -23716,8 +24750,8 @@ function readModelList(payload) {
23716
24750
  // src/hermes/updates.ts
23717
24751
  import { EventEmitter as EventEmitter3 } from "events";
23718
24752
  import { spawn as spawn3 } from "child_process";
23719
- import { mkdir as mkdir12, readFile as readFile17, rm as rm7 } from "fs/promises";
23720
- import path24 from "path";
24753
+ import { mkdir as mkdir12, readFile as readFile18, rm as rm7 } from "fs/promises";
24754
+ import path25 from "path";
23721
24755
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
23722
24756
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
23723
24757
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -23950,7 +24984,7 @@ async function readRemoteRelease(options, now) {
23950
24984
  }
23951
24985
  }
23952
24986
  function normalizeServerReleaseSnapshot(payload) {
23953
- const snapshot = toRecord18(payload);
24987
+ const snapshot = toRecord19(payload);
23954
24988
  const remote = toNullableRecord(snapshot.remote);
23955
24989
  return {
23956
24990
  remote: remote ? normalizeServerRelease(remote) : null,
@@ -23986,7 +25020,7 @@ async function writeUpdateState(paths, state) {
23986
25020
  await writeJsonFile(updateStatePath(paths), state);
23987
25021
  }
23988
25022
  async function readUpdateLogLines(paths) {
23989
- const raw = await readFile17(updateLogPath(paths), "utf8").catch(() => "");
25023
+ const raw = await readFile18(updateLogPath(paths), "utf8").catch(() => "");
23990
25024
  if (!raw.trim()) {
23991
25025
  return [];
23992
25026
  }
@@ -23995,13 +25029,13 @@ async function readUpdateLogLines(paths) {
23995
25029
  );
23996
25030
  }
23997
25031
  function releaseCachePath(paths) {
23998
- return path24.join(paths.indexesDir, "hermes-release-check.json");
25032
+ return path25.join(paths.indexesDir, "hermes-release-check.json");
23999
25033
  }
24000
25034
  function updateStatePath(paths) {
24001
- return path24.join(paths.runDir, "hermes-update-state.json");
25035
+ return path25.join(paths.runDir, "hermes-update-state.json");
24002
25036
  }
24003
25037
  function updateLogPath(paths) {
24004
- return path24.join(paths.logsDir, UPDATE_LOG_FILE);
25038
+ return path25.join(paths.logsDir, UPDATE_LOG_FILE);
24005
25039
  }
24006
25040
  async function clearUpdateLogFiles(paths) {
24007
25041
  const primary = updateLogPath(paths);
@@ -24039,7 +25073,7 @@ function compareSemver2(left, right) {
24039
25073
  }
24040
25074
  return 0;
24041
25075
  }
24042
- function toRecord18(value) {
25076
+ function toRecord19(value) {
24043
25077
  return typeof value === "object" && value !== null ? value : {};
24044
25078
  }
24045
25079
  function toNullableRecord(value) {
@@ -24101,13 +25135,13 @@ function readString19(payload, key) {
24101
25135
  // src/link/updates.ts
24102
25136
  import { spawn as spawn5 } from "child_process";
24103
25137
  import { EventEmitter as EventEmitter4 } from "events";
24104
- import { mkdir as mkdir15, readFile as readFile19, rm as rm10 } from "fs/promises";
24105
- import path26 from "path";
25138
+ import { mkdir as mkdir15, readFile as readFile20, rm as rm10 } from "fs/promises";
25139
+ import path27 from "path";
24106
25140
 
24107
25141
  // src/daemon/process.ts
24108
25142
  import { spawn as spawn4 } from "child_process";
24109
- import { mkdir as mkdir14, readFile as readFile18, rm as rm9 } from "fs/promises";
24110
- import path25 from "path";
25143
+ import { mkdir as mkdir14, readFile as readFile19, rm as rm9, writeFile as writeFile4 } from "fs/promises";
25144
+ import path26 from "path";
24111
25145
 
24112
25146
  // src/daemon/service.ts
24113
25147
  import { createServer } from "http";
@@ -24116,6 +25150,121 @@ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promise
24116
25150
  // src/relay/control-client.ts
24117
25151
  import WebSocket from "ws";
24118
25152
 
25153
+ // src/relay/reconnect-state.ts
25154
+ var DEFAULT_STORM_WINDOW_MS = 5 * 6e4;
25155
+ var DEFAULT_STORM_DISCONNECT_LIMIT = 8;
25156
+ var DEFAULT_COOLDOWN_MS = 3 * 6e4;
25157
+ var DEFAULT_RELAY_RECONNECT_BASE_MS = 3e3;
25158
+ var DEFAULT_RELAY_RECONNECT_MAX_MS = 6e4;
25159
+ async function readRelayCooldownDelayMs(paths, now = /* @__PURE__ */ new Date()) {
25160
+ const state = await readLinkState(paths);
25161
+ const reconnect = normalizeRelayReconnectState(state.relayReconnect);
25162
+ const cooldownUntilMs = parseTimeMs(reconnect.cooldownUntil);
25163
+ if (!Number.isFinite(cooldownUntilMs)) {
25164
+ return 0;
25165
+ }
25166
+ return Math.max(0, cooldownUntilMs - now.getTime());
25167
+ }
25168
+ async function recordRelayDisconnect(paths, options = {}) {
25169
+ const now = options.now ?? /* @__PURE__ */ new Date();
25170
+ const nowMs = now.getTime();
25171
+ const stormWindowMs = positiveInteger(options.stormWindowMs, DEFAULT_STORM_WINDOW_MS);
25172
+ const stormDisconnectLimit = positiveInteger(
25173
+ options.stormDisconnectLimit,
25174
+ DEFAULT_STORM_DISCONNECT_LIMIT
25175
+ );
25176
+ const cooldownMs = positiveInteger(options.cooldownMs, DEFAULT_COOLDOWN_MS);
25177
+ let result = {
25178
+ disconnectCount: 0,
25179
+ cooldownUntilMs: null
25180
+ };
25181
+ await updateRelayReconnectState(paths, (current) => {
25182
+ const recentDisconnects = [
25183
+ ...current.recentDisconnects.filter((value) => {
25184
+ const timestamp = parseTimeMs(value);
25185
+ return Number.isFinite(timestamp) && nowMs - timestamp <= stormWindowMs;
25186
+ }),
25187
+ now.toISOString()
25188
+ ];
25189
+ const enteredCooldown = recentDisconnects.length >= stormDisconnectLimit;
25190
+ const cooldownUntil = enteredCooldown ? new Date(nowMs + cooldownMs).toISOString() : current.cooldownUntil;
25191
+ const cooldownUntilMs = enteredCooldown ? nowMs + cooldownMs : null;
25192
+ result = {
25193
+ disconnectCount: recentDisconnects.length,
25194
+ cooldownUntilMs
25195
+ };
25196
+ return {
25197
+ recentDisconnects: enteredCooldown ? [] : recentDisconnects,
25198
+ cooldownUntil,
25199
+ lastFailureAt: now.toISOString(),
25200
+ lastFailureReason: normalizeReason(options.reason)
25201
+ };
25202
+ });
25203
+ return result;
25204
+ }
25205
+ async function clearRelayReconnectState(paths) {
25206
+ await updateRelayReconnectState(paths, (current) => ({
25207
+ ...current,
25208
+ recentDisconnects: [],
25209
+ cooldownUntil: null
25210
+ }));
25211
+ }
25212
+ function computeRelayBackoffMs(attempt, options = {}) {
25213
+ const baseMs = positiveInteger(options.baseMs, DEFAULT_RELAY_RECONNECT_BASE_MS);
25214
+ const maxMs = positiveInteger(options.maxMs, DEFAULT_RELAY_RECONNECT_MAX_MS);
25215
+ const normalizedAttempt = Math.max(1, Math.floor(attempt));
25216
+ const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, normalizedAttempt - 1));
25217
+ const random = options.random ?? Math.random;
25218
+ const ratio = 0.2 + clampRandom(random()) * 0.1;
25219
+ return exponential + Math.floor(exponential * ratio);
25220
+ }
25221
+ async function updateRelayReconnectState(paths, update) {
25222
+ const state = await readLinkState(paths);
25223
+ const next = {
25224
+ ...state,
25225
+ relayReconnect: update(normalizeRelayReconnectState(state.relayReconnect))
25226
+ };
25227
+ await writeJsonFile(paths.stateFile, next);
25228
+ }
25229
+ async function readLinkState(paths) {
25230
+ const state = await readJsonFile(paths.stateFile);
25231
+ return state && typeof state === "object" ? state : {};
25232
+ }
25233
+ function normalizeRelayReconnectState(value) {
25234
+ const record = value && typeof value === "object" ? value : {};
25235
+ return {
25236
+ recentDisconnects: normalizeTimestamps(record.recentDisconnects),
25237
+ cooldownUntil: typeof record.cooldownUntil === "string" ? record.cooldownUntil : null,
25238
+ lastFailureAt: typeof record.lastFailureAt === "string" ? record.lastFailureAt : null,
25239
+ lastFailureReason: typeof record.lastFailureReason === "string" ? record.lastFailureReason : null
25240
+ };
25241
+ }
25242
+ function normalizeTimestamps(value) {
25243
+ if (!Array.isArray(value)) {
25244
+ return [];
25245
+ }
25246
+ return value.filter((item) => typeof item === "string" && Number.isFinite(parseTimeMs(item)));
25247
+ }
25248
+ function normalizeReason(value) {
25249
+ if (typeof value !== "string") {
25250
+ return null;
25251
+ }
25252
+ const trimmed = value.trim();
25253
+ return trimmed ? trimmed.slice(0, 240) : null;
25254
+ }
25255
+ function positiveInteger(value, fallback) {
25256
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
25257
+ }
25258
+ function parseTimeMs(value) {
25259
+ return typeof value === "string" ? Date.parse(value) : Number.NaN;
25260
+ }
25261
+ function clampRandom(value) {
25262
+ if (!Number.isFinite(value)) {
25263
+ return 0;
25264
+ }
25265
+ return Math.min(1, Math.max(0, value));
25266
+ }
25267
+
24119
25268
  // src/relay/stream-policy.ts
24120
25269
  var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
24121
25270
  flushIntervalMs: 1e3,
@@ -24186,23 +25335,76 @@ function connectRelayControl(options) {
24186
25335
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
24187
25336
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
24188
25337
  wsUrl.searchParams.set("link_id", options.linkId);
24189
- const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
24190
- const backoffBaseMs = options.backoffBaseMs ?? 1e3;
24191
- const backoffMaxMs = options.backoffMaxMs ?? 3e4;
25338
+ const paths = options.paths ?? resolveRuntimePaths();
25339
+ const maxReconnectAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
25340
+ const backoffBaseMs = options.backoffBaseMs ?? DEFAULT_RELAY_RECONNECT_BASE_MS;
25341
+ const backoffMaxMs = options.backoffMaxMs ?? DEFAULT_RELAY_RECONNECT_MAX_MS;
24192
25342
  let reconnectAttempts = 0;
24193
25343
  let closedByUser = false;
24194
25344
  let socket = null;
24195
25345
  let retryTimer = null;
24196
25346
  let abortControllers = /* @__PURE__ */ new Map();
24197
25347
  let fatalRelayRejection = null;
25348
+ let relayRetryAfterMs = null;
24198
25349
  let latestNetworkRoutes = null;
24199
25350
  const streamBatchPolicy = {
24200
25351
  current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
24201
25352
  onUpdate: options.onStreamBatchPolicy
24202
25353
  };
25354
+ const startConnect = () => {
25355
+ void waitForPersistedCooldown().then((delay3) => {
25356
+ if (closedByUser) {
25357
+ return;
25358
+ }
25359
+ if (delay3 > 0) {
25360
+ scheduleTimer(delay3, "cooldown", `Relay reconnect cooldown active for ${delay3}ms`);
25361
+ return;
25362
+ }
25363
+ connect();
25364
+ }).catch((error) => {
25365
+ if (closedByUser) {
25366
+ return;
25367
+ }
25368
+ const message = error instanceof Error ? error.message : String(error);
25369
+ scheduleTimer(backoffBaseMs, "retrying", `Relay connect setup failed: ${message}`);
25370
+ });
25371
+ };
24203
25372
  const connect = () => {
24204
25373
  options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
24205
25374
  fatalRelayRejection = null;
25375
+ relayRetryAfterMs = null;
25376
+ let closeHandled = false;
25377
+ const handleConnectionClosed = (reason) => {
25378
+ if (closeHandled) {
25379
+ return;
25380
+ }
25381
+ closeHandled = true;
25382
+ abortAll(abortControllers);
25383
+ abortControllers = /* @__PURE__ */ new Map();
25384
+ if (fatalRelayRejection) {
25385
+ options.onStatus?.({
25386
+ state: "failed",
25387
+ attempt: reconnectAttempts,
25388
+ message: fatalRelayRejection
25389
+ });
25390
+ return;
25391
+ }
25392
+ if (closedByUser) {
25393
+ options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
25394
+ return;
25395
+ }
25396
+ if (Number.isFinite(maxReconnectAttempts) && reconnectAttempts >= maxReconnectAttempts) {
25397
+ options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
25398
+ return;
25399
+ }
25400
+ void scheduleReconnect(reason).catch((error) => {
25401
+ if (closedByUser) {
25402
+ return;
25403
+ }
25404
+ const message = error instanceof Error ? error.message : String(error);
25405
+ scheduleTimer(backoffMaxMs, "retrying", `Relay reconnect scheduling failed: ${message}`);
25406
+ });
25407
+ };
24206
25408
  socket = new WebSocket(wsUrl, {
24207
25409
  headers: {
24208
25410
  "x-hermes-link-version": LINK_VERSION
@@ -24210,6 +25412,7 @@ function connectRelayControl(options) {
24210
25412
  });
24211
25413
  socket.on("open", () => {
24212
25414
  reconnectAttempts = 0;
25415
+ void clearRelayReconnectState(paths).catch(() => void 0);
24213
25416
  options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
24214
25417
  const currentSocket = socket;
24215
25418
  if (currentSocket && latestNetworkRoutes) {
@@ -24225,6 +25428,20 @@ function connectRelayControl(options) {
24225
25428
  socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
24226
25429
  });
24227
25430
  });
25431
+ socket.on("unexpected-response", (request, response) => {
25432
+ const statusCode = response.statusCode ?? 0;
25433
+ fatalRelayRejection = resolveFatalRelayRejectionFromStatus(statusCode);
25434
+ relayRetryAfterMs = readRetryAfterMs(response);
25435
+ const message = fatalRelayRejection ?? `Relay returned HTTP ${statusCode || "unknown"}`;
25436
+ options.onStatus?.({
25437
+ state: "disconnected",
25438
+ attempt: reconnectAttempts,
25439
+ message
25440
+ });
25441
+ response.resume();
25442
+ handleConnectionClosed(message);
25443
+ request.destroy();
25444
+ });
24228
25445
  socket.on("error", (error) => {
24229
25446
  const message = error instanceof Error ? error.message : "Relay websocket error";
24230
25447
  fatalRelayRejection = resolveFatalRelayRejection(message);
@@ -24235,32 +25452,40 @@ function connectRelayControl(options) {
24235
25452
  });
24236
25453
  });
24237
25454
  socket.on("close", () => {
24238
- abortAll(abortControllers);
24239
- abortControllers = /* @__PURE__ */ new Map();
24240
- if (fatalRelayRejection) {
24241
- options.onStatus?.({
24242
- state: "failed",
24243
- attempt: reconnectAttempts,
24244
- message: fatalRelayRejection
24245
- });
24246
- return;
24247
- }
24248
- if (closedByUser) {
24249
- options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
24250
- return;
24251
- }
24252
- if (reconnectAttempts >= maxReconnectAttempts) {
24253
- options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
24254
- return;
24255
- }
24256
- reconnectAttempts += 1;
24257
- const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
24258
- options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
24259
- retryTimer = setTimeout(connect, delay3);
24260
- retryTimer.unref?.();
25455
+ handleConnectionClosed();
24261
25456
  });
24262
25457
  };
24263
- connect();
25458
+ startConnect();
25459
+ async function scheduleReconnect(reason) {
25460
+ const recorded = await recordRelayDisconnect(paths, { reason }).catch(() => ({
25461
+ disconnectCount: 0,
25462
+ cooldownUntilMs: null
25463
+ }));
25464
+ if (closedByUser) {
25465
+ return;
25466
+ }
25467
+ if (recorded.cooldownUntilMs !== null) {
25468
+ reconnectAttempts = 0;
25469
+ const delay4 = Math.max(0, recorded.cooldownUntilMs - Date.now());
25470
+ scheduleTimer(delay4, "cooldown", `Relay reconnect storm guard active for ${delay4}ms`);
25471
+ return;
25472
+ }
25473
+ reconnectAttempts += 1;
25474
+ const backoffMs = computeRelayBackoffMs(reconnectAttempts, {
25475
+ baseMs: backoffBaseMs,
25476
+ maxMs: backoffMaxMs
25477
+ });
25478
+ const delay3 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
25479
+ scheduleTimer(delay3, "retrying", `Retrying in ${delay3}ms`);
25480
+ }
25481
+ async function waitForPersistedCooldown() {
25482
+ return await readRelayCooldownDelayMs(paths).catch(() => 0);
25483
+ }
25484
+ function scheduleTimer(delay3, state, message) {
25485
+ options.onStatus?.({ state, attempt: reconnectAttempts, message });
25486
+ retryTimer = setTimeout(connect, delay3);
25487
+ retryTimer.unref?.();
25488
+ }
24264
25489
  return {
24265
25490
  publishNetworkRoutes(routes) {
24266
25491
  latestNetworkRoutes = routes;
@@ -24297,7 +25522,12 @@ function sendNetworkRoutes(socket, linkId, routes) {
24297
25522
  }));
24298
25523
  }
24299
25524
  function resolveFatalRelayRejection(message) {
24300
- if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
25525
+ const match = /Unexpected server response:\s*(\d{3})\b/u.exec(message);
25526
+ const statusCode = match ? Number.parseInt(match[1], 10) : Number.NaN;
25527
+ return resolveFatalRelayRejectionFromStatus(statusCode);
25528
+ }
25529
+ function resolveFatalRelayRejectionFromStatus(statusCode) {
25530
+ if (!Number.isFinite(statusCode) || ![400, 401, 403, 410, 426].includes(statusCode)) {
24301
25531
  return null;
24302
25532
  }
24303
25533
  return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
@@ -24308,10 +25538,21 @@ function abortAll(abortControllers) {
24308
25538
  }
24309
25539
  abortControllers.clear();
24310
25540
  }
24311
- function computeBackoffMs(attempt, baseMs, maxMs) {
24312
- const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
24313
- const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
24314
- return exponential + jitter;
25541
+ function readRetryAfterMs(response) {
25542
+ const raw = response.headers["retry-after"];
25543
+ const value = Array.isArray(raw) ? raw[0] : raw;
25544
+ if (!value) {
25545
+ return null;
25546
+ }
25547
+ const seconds = Number.parseInt(value, 10);
25548
+ if (Number.isInteger(seconds) && seconds >= 0) {
25549
+ return seconds * 1e3;
25550
+ }
25551
+ const dateMs = Date.parse(value);
25552
+ if (!Number.isFinite(dateMs)) {
25553
+ return null;
25554
+ }
25555
+ return Math.max(0, dateMs - Date.now());
24315
25556
  }
24316
25557
  async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
24317
25558
  const frame = JSON.parse(raw);
@@ -24432,10 +25673,58 @@ function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
24432
25673
  };
24433
25674
  }
24434
25675
 
25676
+ // src/relay/status-state.ts
25677
+ async function readRelayStatusSnapshot(paths) {
25678
+ const state = await readLinkState2(paths);
25679
+ return normalizeRelayStatusSnapshot(state.relayStatus);
25680
+ }
25681
+ async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new Date()) {
25682
+ const current = await readLinkState2(paths);
25683
+ await writeJsonFile(paths.stateFile, {
25684
+ ...current,
25685
+ relayStatus: {
25686
+ state: status.state,
25687
+ attempt: Number.isFinite(status.attempt) ? Math.max(0, Math.floor(status.attempt)) : 0,
25688
+ message: normalizeMessage(status.message),
25689
+ updatedAt: now.toISOString()
25690
+ }
25691
+ });
25692
+ }
25693
+ async function readLinkState2(paths) {
25694
+ const state = await readJsonFile(paths.stateFile);
25695
+ return state && typeof state === "object" ? state : {};
25696
+ }
25697
+ function normalizeRelayStatusSnapshot(value) {
25698
+ const record = value && typeof value === "object" ? value : null;
25699
+ if (!record || !isRelayStatusState(record.state)) {
25700
+ return null;
25701
+ }
25702
+ const updatedAt = typeof record.updatedAt === "string" && Number.isFinite(Date.parse(record.updatedAt)) ? record.updatedAt : null;
25703
+ if (!updatedAt) {
25704
+ return null;
25705
+ }
25706
+ return {
25707
+ state: record.state,
25708
+ attempt: typeof record.attempt === "number" && Number.isFinite(record.attempt) ? Math.max(0, Math.floor(record.attempt)) : 0,
25709
+ message: normalizeMessage(record.message),
25710
+ updatedAt
25711
+ };
25712
+ }
25713
+ function isRelayStatusState(value) {
25714
+ return value === "connecting" || value === "connected" || value === "disconnected" || value === "retrying" || value === "cooldown" || value === "failed";
25715
+ }
25716
+ function normalizeMessage(value) {
25717
+ if (typeof value !== "string") {
25718
+ return null;
25719
+ }
25720
+ const trimmed = value.trim();
25721
+ return trimmed ? trimmed.slice(0, 240) : null;
25722
+ }
25723
+
24435
25724
  // src/runtime/system-info.ts
24436
25725
  import { execFileSync } from "child_process";
24437
25726
  import { readFileSync } from "fs";
24438
- import os4 from "os";
25727
+ import os5 from "os";
24439
25728
  function readLinkSystemInfo() {
24440
25729
  const platform = process.platform;
24441
25730
  const hostname = readHostname(platform);
@@ -24474,7 +25763,7 @@ function readHostname(platform) {
24474
25763
  return computerName;
24475
25764
  }
24476
25765
  }
24477
- return normalizeText(os4.hostname());
25766
+ return normalizeText(os5.hostname());
24478
25767
  }
24479
25768
  function readOsLabel(platform) {
24480
25769
  if (platform === "darwin") {
@@ -24482,12 +25771,12 @@ function readOsLabel(platform) {
24482
25771
  return version ? `macOS ${version}` : "macOS";
24483
25772
  }
24484
25773
  if (platform === "linux") {
24485
- return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
25774
+ return readLinuxOsRelease() ?? `Linux ${os5.release()}`;
24486
25775
  }
24487
25776
  if (platform === "win32") {
24488
- return `Windows ${os4.release()}`;
25777
+ return `Windows ${os5.release()}`;
24489
25778
  }
24490
- return `${os4.type()} ${os4.release()}`.trim();
25779
+ return `${os5.type()} ${os5.release()}`.trim();
24491
25780
  }
24492
25781
  function readLinuxOsRelease() {
24493
25782
  for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
@@ -24534,11 +25823,11 @@ function truncateText(value, maxLength) {
24534
25823
  }
24535
25824
 
24536
25825
  // src/topology/network.ts
24537
- import os6 from "os";
25826
+ import os7 from "os";
24538
25827
 
24539
25828
  // src/topology/environment.ts
24540
25829
  import { existsSync, readFileSync as readFileSync2 } from "fs";
24541
- import os5 from "os";
25830
+ import os6 from "os";
24542
25831
  function detectRuntimeEnvironment(env = process.env) {
24543
25832
  if (isWsl(env)) {
24544
25833
  return {
@@ -24567,7 +25856,7 @@ function isWsl(env) {
24567
25856
  if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
24568
25857
  return true;
24569
25858
  }
24570
- const release = os5.release().toLowerCase();
25859
+ const release = os6.release().toLowerCase();
24571
25860
  return release.includes("microsoft") || release.includes("wsl");
24572
25861
  }
24573
25862
  function isContainer(env) {
@@ -24612,7 +25901,7 @@ async function discoverRouteCandidates(options) {
24612
25901
  };
24613
25902
  }
24614
25903
  function discoverLanIps() {
24615
- return discoverLanIpsFromInterfaces(os6.networkInterfaces());
25904
+ return discoverLanIpsFromInterfaces(os7.networkInterfaces());
24616
25905
  }
24617
25906
  function discoverLanIpsFromInterfaces(interfaces) {
24618
25907
  const result = /* @__PURE__ */ new Set();
@@ -24751,7 +26040,7 @@ function unique(values) {
24751
26040
  // src/link/network-report-state.ts
24752
26041
  var DEFAULT_AUTO_DAILY_LIMIT = 20;
24753
26042
  async function readNetworkReportState(paths) {
24754
- const state = await readLinkState(paths);
26043
+ const state = await readLinkState3(paths);
24755
26044
  return normalizeNetworkReportState(state.networkReport);
24756
26045
  }
24757
26046
  async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
@@ -24818,14 +26107,14 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
24818
26107
  };
24819
26108
  }
24820
26109
  async function updateNetworkReportState(paths, update) {
24821
- const state = await readLinkState(paths);
26110
+ const state = await readLinkState3(paths);
24822
26111
  const next = {
24823
26112
  ...state,
24824
26113
  networkReport: update(normalizeNetworkReportState(state.networkReport))
24825
26114
  };
24826
26115
  await writeJsonFile(paths.stateFile, next);
24827
26116
  }
24828
- async function readLinkState(paths) {
26117
+ async function readLinkState3(paths) {
24829
26118
  const state = await readJsonFile(paths.stateFile);
24830
26119
  return state && typeof state === "object" ? state : {};
24831
26120
  }
@@ -25098,6 +26387,89 @@ async function checkLanIpChange(options, context = {}) {
25098
26387
  }
25099
26388
  }
25100
26389
 
26390
+ // src/daemon/process-guard.ts
26391
+ var installed = false;
26392
+ var fatalShutdownInProgress = false;
26393
+ var activeLogger = null;
26394
+ var activeOptions = {};
26395
+ var DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS = 5e3;
26396
+ function installDaemonProcessGuard(logger, options = {}) {
26397
+ activeLogger = logger;
26398
+ activeOptions = options;
26399
+ if (installed) {
26400
+ return;
26401
+ }
26402
+ installed = true;
26403
+ process.on("unhandledRejection", (reason) => {
26404
+ void handleFatalProcessFailure("process_unhandled_rejection", reason);
26405
+ });
26406
+ process.on("uncaughtException", (error) => {
26407
+ void handleFatalProcessFailure("process_uncaught_exception", error);
26408
+ });
26409
+ }
26410
+ async function handleFatalProcessFailure(event, error) {
26411
+ const fields = describeProcessFailure(error);
26412
+ const logger = activeLogger;
26413
+ const options = activeOptions;
26414
+ if (fatalShutdownInProgress) {
26415
+ writeFatalFailureToStderr(event, fields);
26416
+ return;
26417
+ }
26418
+ fatalShutdownInProgress = true;
26419
+ if (logger) {
26420
+ try {
26421
+ await logger.error(event, fields);
26422
+ await logger.flush();
26423
+ } catch {
26424
+ }
26425
+ }
26426
+ writeFatalFailureToStderr(event, fields);
26427
+ if (options.onFatal) {
26428
+ try {
26429
+ await Promise.race([
26430
+ options.onFatal(),
26431
+ wait(options.shutdownTimeoutMs ?? DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS)
26432
+ ]);
26433
+ } catch (shutdownError) {
26434
+ if (logger) {
26435
+ try {
26436
+ await logger.error("process_fatal_shutdown_failed", {
26437
+ error: shutdownError instanceof Error ? shutdownError.message : String(shutdownError)
26438
+ });
26439
+ await logger.flush();
26440
+ } catch {
26441
+ }
26442
+ }
26443
+ }
26444
+ }
26445
+ process.exit(1);
26446
+ }
26447
+ function describeProcessFailure(error) {
26448
+ if (error instanceof Error) {
26449
+ return {
26450
+ message: error.message,
26451
+ ...error.stack ? { stack: error.stack } : {}
26452
+ };
26453
+ }
26454
+ return { message: String(error) };
26455
+ }
26456
+ function writeFatalFailureToStderr(event, fields) {
26457
+ try {
26458
+ process.stderr.write(
26459
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${fields.message}
26460
+ ${fields.stack ?? ""}
26461
+ `
26462
+ );
26463
+ } catch {
26464
+ }
26465
+ }
26466
+ function wait(ms) {
26467
+ return new Promise((resolve) => {
26468
+ const timer = setTimeout(resolve, ms);
26469
+ timer.unref?.();
26470
+ });
26471
+ }
26472
+
25101
26473
  // src/daemon/scheduler.ts
25102
26474
  function startCronDeliveryScheduler(options) {
25103
26475
  let running = false;
@@ -25183,6 +26555,11 @@ async function startLinkService(options = {}) {
25183
26555
  current_version: migration.currentVersion
25184
26556
  });
25185
26557
  }
26558
+ await ensureHermesLinkSkillInstalledBestEffort({
26559
+ paths,
26560
+ logger,
26561
+ source: "service_startup"
26562
+ });
25186
26563
  const conversations = new ConversationService(paths, logger);
25187
26564
  await conversations.rebuildStatisticsIndex();
25188
26565
  let relay = null;
@@ -25214,6 +26591,11 @@ async function startLinkService(options = {}) {
25214
26591
  logger,
25215
26592
  conversations,
25216
26593
  onPairingClaimed: async () => {
26594
+ void ensureHermesLinkSkillInstalledBestEffort({
26595
+ paths,
26596
+ logger,
26597
+ source: "pairing_claimed"
26598
+ });
25217
26599
  triggerHermesSessionSync();
25218
26600
  void loadRelayStreamBatchPolicy("pairing_claimed");
25219
26601
  await options.onPairingClaimed?.();
@@ -25238,6 +26620,38 @@ async function startLinkService(options = {}) {
25238
26620
  error: error.message
25239
26621
  });
25240
26622
  });
26623
+ server.on("clientError", (error, socket) => {
26624
+ if (isExpectedClientDisconnectError3(error, {
26625
+ sse: isActiveSseSocket(socket)
26626
+ })) {
26627
+ socket.destroy();
26628
+ return;
26629
+ }
26630
+ void logger.warn("client_connection_error", {
26631
+ port: config.port,
26632
+ link_id: identity?.link_id ?? null,
26633
+ error: error.message
26634
+ });
26635
+ if (socket.writable) {
26636
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
26637
+ } else {
26638
+ socket.destroy();
26639
+ }
26640
+ });
26641
+ server.on("connection", (socket) => {
26642
+ socket.on("error", (error) => {
26643
+ if (isExpectedClientDisconnectError3(error, {
26644
+ sse: isActiveSseSocket(socket)
26645
+ })) {
26646
+ return;
26647
+ }
26648
+ void logger.warn("socket_error", {
26649
+ port: config.port,
26650
+ link_id: identity?.link_id ?? null,
26651
+ error: error.message
26652
+ });
26653
+ });
26654
+ });
25241
26655
  void logger.info("service_started", {
25242
26656
  port: config.port,
25243
26657
  link_id: identity?.link_id ?? null
@@ -25263,9 +26677,8 @@ async function startLinkService(options = {}) {
25263
26677
  relayBaseUrl: config.relayBaseUrl,
25264
26678
  linkId: identity.link_id,
25265
26679
  localPort: config.port,
25266
- maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
25267
- backoffBaseMs: 1e3,
25268
- backoffMaxMs: 3e4,
26680
+ paths,
26681
+ maxReconnectAttempts: options.relayMaxReconnectAttempts,
25269
26682
  onStreamBatchPolicy: (policy) => {
25270
26683
  void logger.info("relay_stream_policy_updated", {
25271
26684
  flushIntervalMs: policy.flushIntervalMs,
@@ -25273,6 +26686,7 @@ async function startLinkService(options = {}) {
25273
26686
  });
25274
26687
  },
25275
26688
  onStatus: (status) => {
26689
+ void writeRelayStatusSnapshot(paths, status).catch(() => void 0);
25276
26690
  void logger.info("relay_status", status);
25277
26691
  if (status.state === "connected") {
25278
26692
  const now = Date.now();
@@ -25313,8 +26727,13 @@ async function startLinkService(options = {}) {
25313
26727
  if (options.writePidFile) {
25314
26728
  await writePidFile(paths);
25315
26729
  }
25316
- return {
26730
+ let closed = false;
26731
+ const service = {
25317
26732
  async close() {
26733
+ if (closed) {
26734
+ return;
26735
+ }
26736
+ closed = true;
25318
26737
  relay?.close();
25319
26738
  await closeServer(server);
25320
26739
  await Promise.all([
@@ -25330,6 +26749,12 @@ async function startLinkService(options = {}) {
25330
26749
  }
25331
26750
  }
25332
26751
  };
26752
+ if (options.writePidFile) {
26753
+ installDaemonProcessGuard(logger, {
26754
+ onFatal: () => service.close()
26755
+ });
26756
+ }
26757
+ return service;
25333
26758
  }
25334
26759
  function waitForRelayReadyTimeout(timeoutMs) {
25335
26760
  return new Promise((resolve) => {
@@ -25384,6 +26809,16 @@ async function closeServer(server) {
25384
26809
  server.closeIdleConnections?.();
25385
26810
  });
25386
26811
  }
26812
+ function isExpectedClientDisconnectError3(error, options = {}) {
26813
+ if (!(error instanceof Error)) {
26814
+ return false;
26815
+ }
26816
+ const code = String(error.code ?? "");
26817
+ if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted)/iu.test(error.message)) {
26818
+ return true;
26819
+ }
26820
+ return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
26821
+ }
25387
26822
  async function listenServer(server, port) {
25388
26823
  await new Promise((resolve, reject) => {
25389
26824
  const cleanup = () => {
@@ -25405,6 +26840,16 @@ async function listenServer(server, port) {
25405
26840
  }
25406
26841
 
25407
26842
  // src/daemon/process.ts
26843
+ var SUPERVISOR_RESTART_INITIAL_DELAY_MS = 1e3;
26844
+ var SUPERVISOR_RESTART_MAX_DELAY_MS = 3e4;
26845
+ var SUPERVISOR_STABLE_UPTIME_MS = 6e4;
26846
+ var SUPERVISOR_HEALTH_STARTUP_GRACE_MS = 15e3;
26847
+ var SUPERVISOR_HEALTH_INTERVAL_MS = 15e3;
26848
+ var SUPERVISOR_HEALTH_TIMEOUT_MS = 3e3;
26849
+ var SUPERVISOR_HEALTH_FAILURE_THRESHOLD = 3;
26850
+ var SUPERVISOR_CHILD_STOP_TIMEOUT_MS = 5e3;
26851
+ var SUPERVISOR_STOP_INTENT_TTL_MS = 2 * 6e4;
26852
+ var INTERNAL_HEALTH_PROBE_HEADER2 = "x-hermes-link-internal-health-probe";
25408
26853
  async function startDaemonProcess(paths = resolveRuntimePaths()) {
25409
26854
  const config = await loadConfig(paths);
25410
26855
  let status = await getDaemonStatus(paths);
@@ -25429,7 +26874,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
25429
26874
  });
25430
26875
  child.unref();
25431
26876
  for (let index = 0; index < 12; index += 1) {
25432
- await wait(250);
26877
+ await wait2(250);
25433
26878
  const next = await getDaemonStatus(paths);
25434
26879
  if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
25435
26880
  return next;
@@ -25441,43 +26886,92 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
25441
26886
  await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
25442
26887
  const log = createRotatingTextLogWriter({
25443
26888
  paths,
25444
- fileName: path25.basename(daemonLogFile(paths))
26889
+ fileName: path26.basename(daemonLogFile(paths))
25445
26890
  });
25446
26891
  const scriptPath = currentCliScriptPath();
25447
- const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
25448
- stdio: ["ignore", "pipe", "pipe"],
25449
- env: process.env
25450
- });
25451
26892
  const write = (chunk) => {
25452
26893
  void log.write(chunk);
25453
26894
  };
25454
26895
  write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
25455
26896
  `);
25456
- child.stdout?.on("data", write);
25457
- child.stderr?.on("data", write);
26897
+ await clearExpiredSupervisorStopIntent(paths).catch(() => void 0);
26898
+ const logSupervisorUnhandledRejection = (reason) => {
26899
+ writeSupervisorFailure(write, "supervisor_unhandled_rejection", reason);
26900
+ };
26901
+ const logSupervisorUncaughtException = (error) => {
26902
+ writeSupervisorFailure(write, "supervisor_uncaught_exception", error);
26903
+ };
26904
+ let child = null;
26905
+ let stopRequested = false;
25458
26906
  const forwardStop = () => {
25459
- if (child.pid && isProcessAlive3(child.pid)) {
26907
+ stopRequested = true;
26908
+ if (child?.pid && isProcessAlive3(child.pid)) {
25460
26909
  child.kill("SIGTERM");
25461
26910
  }
25462
26911
  };
25463
26912
  process.once("SIGINT", forwardStop);
25464
26913
  process.once("SIGTERM", forwardStop);
25465
- const result = await new Promise((resolve, reject) => {
25466
- child.once("error", reject);
25467
- child.once("exit", (code, signal) => resolve({ code, signal }));
25468
- }).catch((error) => {
25469
- write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
25470
- `);
25471
- return { code: 1, signal: null };
25472
- });
25473
- process.off("SIGINT", forwardStop);
25474
- process.off("SIGTERM", forwardStop);
26914
+ process.on("unhandledRejection", logSupervisorUnhandledRejection);
26915
+ process.on("uncaughtException", logSupervisorUncaughtException);
26916
+ let restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
26917
+ let finalResult = {
26918
+ code: 0,
26919
+ signal: null
26920
+ };
26921
+ try {
26922
+ while (!stopRequested) {
26923
+ const startedAt = Date.now();
26924
+ child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
26925
+ stdio: ["ignore", "pipe", "pipe"],
26926
+ env: process.env
26927
+ });
26928
+ const childPid = child.pid ?? null;
26929
+ child.stdout?.on("data", write);
26930
+ child.stderr?.on("data", write);
26931
+ const healthMonitor = startSupervisorHealthMonitor(paths, child, write);
26932
+ const result = await new Promise((resolve, reject) => {
26933
+ child?.once("error", reject);
26934
+ child?.once("exit", (code, signal) => resolve({ code, signal }));
26935
+ }).catch((error) => {
26936
+ write(
26937
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child failed to start: ${error instanceof Error ? error.message : String(error)}
26938
+ `
26939
+ );
26940
+ return { code: 1, signal: null };
26941
+ });
26942
+ healthMonitor.close();
26943
+ finalResult = result;
26944
+ child = null;
26945
+ const expectedStop = childPid !== null && await consumeSupervisorStopIntent(paths, childPid);
26946
+ if (stopRequested || expectedStop || result.code === 0 || isIntentionalStopSignal(result.signal)) {
26947
+ break;
26948
+ }
26949
+ const uptimeMs = Date.now() - startedAt;
26950
+ if (uptimeMs >= SUPERVISOR_STABLE_UPTIME_MS) {
26951
+ restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
26952
+ }
26953
+ write(
26954
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child stopped unexpectedly code=${result.code ?? "null"} signal=${result.signal ?? "null"}; restarting in ${restartDelayMs}ms
26955
+ `
26956
+ );
26957
+ await wait2(restartDelayMs);
26958
+ restartDelayMs = Math.min(
26959
+ restartDelayMs * 2,
26960
+ SUPERVISOR_RESTART_MAX_DELAY_MS
26961
+ );
26962
+ }
26963
+ } finally {
26964
+ process.off("SIGINT", forwardStop);
26965
+ process.off("SIGTERM", forwardStop);
26966
+ process.off("unhandledRejection", logSupervisorUnhandledRejection);
26967
+ process.off("uncaughtException", logSupervisorUncaughtException);
26968
+ }
25475
26969
  write(
25476
- `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${result.code ?? "null"} signal=${result.signal ?? "null"}
26970
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${finalResult.code ?? "null"} signal=${finalResult.signal ?? "null"}
25477
26971
  `
25478
26972
  );
25479
26973
  await log.flush();
25480
- return result.code ?? (result.signal ? 0 : 1);
26974
+ return finalResult.code ?? (finalResult.signal ? 0 : 1);
25481
26975
  }
25482
26976
  async function probeLocalLinkService(options) {
25483
26977
  const unreachable = {
@@ -25515,6 +27009,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25515
27009
  if (!status.running || !status.pid) {
25516
27010
  return status;
25517
27011
  }
27012
+ await writeSupervisorStopIntent(paths, status.pid).catch(() => void 0);
25518
27013
  try {
25519
27014
  process.kill(status.pid, "SIGTERM");
25520
27015
  } catch {
@@ -25522,7 +27017,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25522
27017
  return await getDaemonStatus(paths);
25523
27018
  }
25524
27019
  for (let index = 0; index < 20; index += 1) {
25525
- await wait(250);
27020
+ await wait2(250);
25526
27021
  if (!isProcessAlive3(status.pid)) {
25527
27022
  break;
25528
27023
  }
@@ -25533,7 +27028,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25533
27028
  } catch {
25534
27029
  }
25535
27030
  for (let index = 0; index < 10; index += 1) {
25536
- await wait(250);
27031
+ await wait2(250);
25537
27032
  if (!isProcessAlive3(status.pid)) {
25538
27033
  break;
25539
27034
  }
@@ -25570,7 +27065,7 @@ function currentCliScriptPath() {
25570
27065
  return process.argv[1];
25571
27066
  }
25572
27067
  async function readPid(filePath) {
25573
- const raw = await readFile18(filePath, "utf8").catch(() => null);
27068
+ const raw = await readFile19(filePath, "utf8").catch(() => null);
25574
27069
  if (!raw) {
25575
27070
  return null;
25576
27071
  }
@@ -25585,6 +27080,171 @@ function isProcessAlive3(pid) {
25585
27080
  return false;
25586
27081
  }
25587
27082
  }
27083
+ function isIntentionalStopSignal(signal) {
27084
+ return signal === "SIGINT" || signal === "SIGTERM" || signal === "SIGKILL";
27085
+ }
27086
+ function startSupervisorHealthMonitor(paths, child, write) {
27087
+ let closed = false;
27088
+ let failureCount = 0;
27089
+ let timer = null;
27090
+ let forceKillTimer = null;
27091
+ const schedule = (delayMs) => {
27092
+ timer = setTimeout(check, delayMs);
27093
+ timer.unref?.();
27094
+ };
27095
+ const check = () => {
27096
+ void probeSupervisorHttpHealth(paths).then((healthy) => {
27097
+ if (closed) {
27098
+ return;
27099
+ }
27100
+ if (healthy) {
27101
+ failureCount = 0;
27102
+ schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
27103
+ return;
27104
+ }
27105
+ failureCount += 1;
27106
+ if (failureCount < SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
27107
+ schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
27108
+ return;
27109
+ }
27110
+ closed = true;
27111
+ write(
27112
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed ${failureCount} consecutive times; restarting child
27113
+ `
27114
+ );
27115
+ terminateChild(child, forceKillTimer);
27116
+ forceKillTimer = setTimeout(() => {
27117
+ if (child.pid && isProcessAlive3(child.pid)) {
27118
+ child.kill("SIGKILL");
27119
+ }
27120
+ }, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
27121
+ forceKillTimer.unref?.();
27122
+ }).catch((error) => {
27123
+ if (closed) {
27124
+ return;
27125
+ }
27126
+ failureCount += 1;
27127
+ write(
27128
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed: ${error instanceof Error ? error.message : String(error)}
27129
+ `
27130
+ );
27131
+ if (failureCount >= SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
27132
+ closed = true;
27133
+ terminateChild(child, forceKillTimer);
27134
+ forceKillTimer = setTimeout(() => {
27135
+ if (child.pid && isProcessAlive3(child.pid)) {
27136
+ child.kill("SIGKILL");
27137
+ }
27138
+ }, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
27139
+ forceKillTimer.unref?.();
27140
+ return;
27141
+ }
27142
+ schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
27143
+ });
27144
+ };
27145
+ schedule(SUPERVISOR_HEALTH_STARTUP_GRACE_MS);
27146
+ return {
27147
+ close() {
27148
+ closed = true;
27149
+ if (timer) {
27150
+ clearTimeout(timer);
27151
+ timer = null;
27152
+ }
27153
+ if (forceKillTimer) {
27154
+ clearTimeout(forceKillTimer);
27155
+ forceKillTimer = null;
27156
+ }
27157
+ }
27158
+ };
27159
+ }
27160
+ async function probeSupervisorHttpHealth(paths) {
27161
+ const config = await loadConfig(paths).catch(() => null);
27162
+ if (!config) {
27163
+ return false;
27164
+ }
27165
+ try {
27166
+ const response = await fetch(`http://127.0.0.1:${config.port}/api/v1/bootstrap`, {
27167
+ headers: {
27168
+ accept: "application/json",
27169
+ [INTERNAL_HEALTH_PROBE_HEADER2]: "1"
27170
+ },
27171
+ signal: AbortSignal.timeout(SUPERVISOR_HEALTH_TIMEOUT_MS)
27172
+ });
27173
+ return response.ok;
27174
+ } catch {
27175
+ return false;
27176
+ }
27177
+ }
27178
+ function terminateChild(child, previousForceKillTimer) {
27179
+ if (previousForceKillTimer) {
27180
+ clearTimeout(previousForceKillTimer);
27181
+ }
27182
+ if (child.pid && isProcessAlive3(child.pid)) {
27183
+ child.kill("SIGTERM");
27184
+ }
27185
+ }
27186
+ function supervisorStopIntentPath(paths) {
27187
+ return path26.join(paths.runDir, "supervisor-stop-intent.json");
27188
+ }
27189
+ async function writeSupervisorStopIntent(paths, pid) {
27190
+ await mkdir14(paths.runDir, { recursive: true, mode: 448 });
27191
+ await writeFile4(
27192
+ supervisorStopIntentPath(paths),
27193
+ `${JSON.stringify({ pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
27194
+ `,
27195
+ { mode: 384 }
27196
+ );
27197
+ }
27198
+ async function consumeSupervisorStopIntent(paths, pid) {
27199
+ const filePath = supervisorStopIntentPath(paths);
27200
+ const raw = await readFile19(filePath, "utf8").catch(() => null);
27201
+ if (!raw) {
27202
+ return false;
27203
+ }
27204
+ const payload = parseSupervisorStopIntent(raw);
27205
+ if (!isValidSupervisorStopIntent(payload)) {
27206
+ await rm9(filePath, { force: true }).catch(() => void 0);
27207
+ return false;
27208
+ }
27209
+ if (payload.pid !== pid) {
27210
+ await rm9(filePath, { force: true }).catch(() => void 0);
27211
+ return false;
27212
+ }
27213
+ await rm9(filePath, { force: true }).catch(() => void 0);
27214
+ return true;
27215
+ }
27216
+ async function clearExpiredSupervisorStopIntent(paths) {
27217
+ const filePath = supervisorStopIntentPath(paths);
27218
+ const raw = await readFile19(filePath, "utf8").catch(() => null);
27219
+ if (!raw) {
27220
+ return;
27221
+ }
27222
+ const payload = parseSupervisorStopIntent(raw);
27223
+ if (!isValidSupervisorStopIntent(payload)) {
27224
+ await rm9(filePath, { force: true }).catch(() => void 0);
27225
+ }
27226
+ }
27227
+ function parseSupervisorStopIntent(raw) {
27228
+ try {
27229
+ return JSON.parse(raw);
27230
+ } catch {
27231
+ return {};
27232
+ }
27233
+ }
27234
+ function isValidSupervisorStopIntent(payload) {
27235
+ if (typeof payload.pid !== "number" || !Number.isInteger(payload.pid) || typeof payload.created_at !== "string") {
27236
+ return false;
27237
+ }
27238
+ const createdAtMs = Date.parse(payload.created_at);
27239
+ return Number.isFinite(createdAtMs) && Date.now() - createdAtMs <= SUPERVISOR_STOP_INTENT_TTL_MS;
27240
+ }
27241
+ function writeSupervisorFailure(write, event, error) {
27242
+ const message = error instanceof Error ? error.message : String(error);
27243
+ const stack = error instanceof Error && error.stack ? `
27244
+ ${error.stack}` : "";
27245
+ write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${message}${stack}
27246
+ `);
27247
+ }
25588
27248
  async function pidBackedServiceIsReachable(paths) {
25589
27249
  const config = await loadConfig(paths).catch(() => null);
25590
27250
  if (!config) {
@@ -25592,7 +27252,7 @@ async function pidBackedServiceIsReachable(paths) {
25592
27252
  }
25593
27253
  return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
25594
27254
  }
25595
- function wait(ms) {
27255
+ function wait2(ms) {
25596
27256
  return new Promise((resolve) => setTimeout(resolve, ms));
25597
27257
  }
25598
27258
 
@@ -25999,7 +27659,7 @@ async function readRemoteLinkPolicy(options) {
25999
27659
  }
26000
27660
  }
26001
27661
  function normalizeServerSnapshot(payload) {
26002
- const snapshot = toRecord19(payload);
27662
+ const snapshot = toRecord20(payload);
26003
27663
  const policy = toNullableRecord2(snapshot.policy);
26004
27664
  if (!policy) {
26005
27665
  return {
@@ -26342,7 +28002,7 @@ async function writeUpdateState2(paths, state) {
26342
28002
  await writeJsonFile(updateStatePath2(paths), state);
26343
28003
  }
26344
28004
  async function readUpdateLogLines2(paths) {
26345
- const raw = await readFile19(updateLogPath2(paths), "utf8").catch(() => "");
28005
+ const raw = await readFile20(updateLogPath2(paths), "utf8").catch(() => "");
26346
28006
  if (!raw.trim()) {
26347
28007
  return [];
26348
28008
  }
@@ -26351,10 +28011,10 @@ async function readUpdateLogLines2(paths) {
26351
28011
  );
26352
28012
  }
26353
28013
  function updateStatePath2(paths) {
26354
- return path26.join(paths.runDir, "link-update-state.json");
28014
+ return path27.join(paths.runDir, "link-update-state.json");
26355
28015
  }
26356
28016
  function updateLogPath2(paths) {
26357
- return path26.join(paths.logsDir, UPDATE_LOG_FILE2);
28017
+ return path27.join(paths.logsDir, UPDATE_LOG_FILE2);
26358
28018
  }
26359
28019
  async function clearUpdateLogFiles2(paths) {
26360
28020
  const primary = updateLogPath2(paths);
@@ -26426,7 +28086,7 @@ function isProcessAlive4(pid) {
26426
28086
  return false;
26427
28087
  }
26428
28088
  }
26429
- function toRecord19(value) {
28089
+ function toRecord20(value) {
26430
28090
  return typeof value === "object" && value !== null ? value : {};
26431
28091
  }
26432
28092
  function toNullableRecord2(value) {
@@ -26438,7 +28098,7 @@ function readString20(payload, key) {
26438
28098
  }
26439
28099
 
26440
28100
  // src/pairing/pairing.ts
26441
- import path27 from "path";
28101
+ import path28 from "path";
26442
28102
  import { rm as rm11 } from "fs/promises";
26443
28103
 
26444
28104
  // src/relay/bootstrap.ts
@@ -26778,10 +28438,10 @@ async function loadRequiredIdentity2(paths) {
26778
28438
  }
26779
28439
  return identity;
26780
28440
  }
26781
- async function postServerJson(serverBaseUrl, path28, body, options) {
28441
+ async function postServerJson(serverBaseUrl, path29, body, options) {
26782
28442
  let response;
26783
28443
  try {
26784
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path28}`, {
28444
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
26785
28445
  method: "POST",
26786
28446
  headers: {
26787
28447
  accept: "application/json",
@@ -26829,10 +28489,10 @@ function pairingErrorSnapshot(stage, error) {
26829
28489
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
26830
28490
  };
26831
28491
  }
26832
- async function patchServerJson(serverBaseUrl, path28, token, body, options) {
28492
+ async function patchServerJson(serverBaseUrl, path29, token, body, options) {
26833
28493
  let response;
26834
28494
  try {
26835
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path28}`, {
28495
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
26836
28496
  method: "PATCH",
26837
28497
  headers: {
26838
28498
  accept: "application/json",
@@ -26880,10 +28540,10 @@ function createPairingNetworkError(input) {
26880
28540
  );
26881
28541
  }
26882
28542
  function pairingClaimPath(sessionId, paths) {
26883
- return path27.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
28543
+ return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
26884
28544
  }
26885
28545
  function pairingSessionPath(sessionId, paths) {
26886
- return path27.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
28546
+ return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
26887
28547
  }
26888
28548
  function qrPreferredUrls(routes) {
26889
28549
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -26997,7 +28657,12 @@ function registerSystemRoutes(router, options) {
26997
28657
  error: error instanceof Error ? error.message : String(error)
26998
28658
  });
26999
28659
  });
27000
- void options.onPairingClaimed?.();
28660
+ void Promise.resolve().then(() => options.onPairingClaimed?.()).catch((error) => {
28661
+ void logger.warn("pairing_claim_callback_failed", {
28662
+ session_id: sessionId,
28663
+ error: error instanceof Error ? error.message : String(error)
28664
+ });
28665
+ });
27001
28666
  }, 250);
27002
28667
  timer.unref?.();
27003
28668
  });
@@ -28035,6 +29700,18 @@ async function createApp(options = {}) {
28035
29700
  };
28036
29701
  const app = new Koa();
28037
29702
  const router = new Router();
29703
+ app.on("error", (error, ctx) => {
29704
+ if (isExpectedClientDisconnectError2(error, {
29705
+ sse: isSseRequestContext(ctx)
29706
+ })) {
29707
+ return;
29708
+ }
29709
+ void logger.error("http_app_error", {
29710
+ method: ctx?.method ?? null,
29711
+ path: ctx?.path ?? null,
29712
+ error: error instanceof Error ? error.message : String(error)
29713
+ });
29714
+ });
28038
29715
  app.use(createHttpErrorMiddleware(logger));
28039
29716
  registerSystemRoutes(router, {
28040
29717
  paths,
@@ -28073,6 +29750,11 @@ export {
28073
29750
  resolveRuntimePaths,
28074
29751
  createFileLogger,
28075
29752
  getLinkLogFile,
29753
+ readRecentLogEntries,
29754
+ readRecentTextLogEntries,
29755
+ getGatewayLogFiles,
29756
+ readRecentGatewayLogEntries,
29757
+ flushLogFiles,
28076
29758
  ensureHermesApiServerAvailable,
28077
29759
  readHermesVersion,
28078
29760
  defaultLinkConfig,
@@ -28085,6 +29767,7 @@ export {
28085
29767
  getIdentityStatus,
28086
29768
  ConversationService,
28087
29769
  hasActiveDevices,
29770
+ ensureHermesLinkSkillInstalledBestEffort,
28088
29771
  detectRuntimeEnvironment,
28089
29772
  preparePairing,
28090
29773
  readPairingClaim,
@@ -28092,6 +29775,7 @@ export {
28092
29775
  createApp,
28093
29776
  fetchRelayStreamBatchPolicy,
28094
29777
  connectRelayControl,
29778
+ readRelayStatusSnapshot,
28095
29779
  reportLinkStatusToServer,
28096
29780
  startLinkService,
28097
29781
  startDaemonProcess,