@hermespilot/link 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.5.9";
4868
+ var LINK_VERSION = "0.6.1";
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 } : {},
@@ -13353,6 +13863,17 @@ async function buildConversationHistory(input) {
13353
13863
  )
13354
13864
  };
13355
13865
  }
13866
+ if (snapshotHistory.length > 0) {
13867
+ return {
13868
+ messages: snapshotHistory,
13869
+ source: "link_snapshot",
13870
+ diagnostics: createHistoryDiagnostics(
13871
+ snapshotHistory,
13872
+ "link_snapshot_authoritative",
13873
+ { snapshot_message_count: snapshotHistory.length }
13874
+ )
13875
+ };
13876
+ }
13356
13877
  const hermesHistory = await readHermesTranscriptHistory(
13357
13878
  input.hermesSessionId,
13358
13879
  input.profileName
@@ -13396,11 +13917,16 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
13396
13917
  }));
13397
13918
  const [dbHistory, jsonlHistory] = await Promise.all([
13398
13919
  readHermesStateDbHistory(dbPath, normalizedSessionId),
13399
- readHermesJsonlHistory(sessionsDirConfig.sessionsDir, normalizedSessionId)
13920
+ readHermesTranscriptFilesHistory(
13921
+ sessionsDirConfig.sessionsDir,
13922
+ normalizedSessionId
13923
+ )
13400
13924
  ]);
13401
13925
  const diagnosticCounts = {
13402
13926
  state_db_message_count: dbHistory.length,
13403
- jsonl_message_count: jsonlHistory.messages.length,
13927
+ transcript_message_count: jsonlHistory.messages.length,
13928
+ session_json_message_count: jsonlHistory.sessionJsonMessageCount,
13929
+ jsonl_message_count: jsonlHistory.jsonlMessageCount,
13404
13930
  jsonl_byte_count: jsonlHistory.byteCount,
13405
13931
  jsonl_line_count: jsonlHistory.lineCount,
13406
13932
  jsonl_skipped_line_count: jsonlHistory.skippedLineCount,
@@ -13413,7 +13939,7 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
13413
13939
  source: "hermes_transcript",
13414
13940
  diagnostics: createHistoryDiagnostics(
13415
13941
  jsonlHistory.messages,
13416
- "jsonl_has_more_messages",
13942
+ "transcript_has_more_messages",
13417
13943
  diagnosticCounts
13418
13944
  )
13419
13945
  };
@@ -13448,13 +13974,9 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
13448
13974
  if (!isValidSessionFileStem(sessionId)) {
13449
13975
  return empty;
13450
13976
  }
13451
- const transcriptPath = path17.join(sessionsDir, `${sessionId}.jsonl`);
13452
- const raw = await readFile10(transcriptPath, "utf8").catch((error) => {
13453
- if (isNodeError12(error, "ENOENT")) {
13454
- return "";
13455
- }
13456
- throw error;
13457
- });
13977
+ const raw = await readFirstExistingFile(
13978
+ candidateTranscriptPaths(sessionsDir, sessionId, "jsonl")
13979
+ );
13458
13980
  if (!raw.trim()) {
13459
13981
  return empty;
13460
13982
  }
@@ -13482,6 +14004,93 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
13482
14004
  skippedLineCount
13483
14005
  };
13484
14006
  }
14007
+ async function readHermesSessionJsonHistory(sessionsDir, sessionId) {
14008
+ const empty = {
14009
+ messages: [],
14010
+ byteCount: 0,
14011
+ skippedMessageCount: 0
14012
+ };
14013
+ if (!isValidSessionFileStem(sessionId)) {
14014
+ return empty;
14015
+ }
14016
+ const raw = await readFirstExistingFile(
14017
+ candidateTranscriptPaths(sessionsDir, sessionId, "json")
14018
+ );
14019
+ if (!raw.trim()) {
14020
+ return empty;
14021
+ }
14022
+ let payload;
14023
+ try {
14024
+ payload = JSON.parse(raw);
14025
+ } catch {
14026
+ return {
14027
+ messages: [],
14028
+ byteCount: Buffer.byteLength(raw, "utf8"),
14029
+ skippedMessageCount: 1
14030
+ };
14031
+ }
14032
+ const records = Array.isArray(payload) ? payload : isRecord2(payload) && Array.isArray(payload.messages) ? payload.messages : [];
14033
+ const messages = [];
14034
+ let skippedMessageCount = 0;
14035
+ for (const record of records) {
14036
+ if (!isRecord2(record)) {
14037
+ skippedMessageCount += 1;
14038
+ continue;
14039
+ }
14040
+ const message = normalizeHistoryRecord(record);
14041
+ if (message) {
14042
+ messages.push(message);
14043
+ } else {
14044
+ skippedMessageCount += 1;
14045
+ }
14046
+ }
14047
+ return {
14048
+ messages,
14049
+ byteCount: Buffer.byteLength(raw, "utf8"),
14050
+ skippedMessageCount
14051
+ };
14052
+ }
14053
+ async function readHermesTranscriptFilesHistory(sessionsDir, sessionId) {
14054
+ const [jsonHistory, jsonlHistory] = await Promise.all([
14055
+ readHermesSessionJsonHistory(sessionsDir, sessionId),
14056
+ readHermesJsonlHistory(sessionsDir, sessionId)
14057
+ ]);
14058
+ if (jsonHistory.messages.length > jsonlHistory.messages.length) {
14059
+ return {
14060
+ messages: jsonHistory.messages,
14061
+ byteCount: jsonHistory.byteCount,
14062
+ lineCount: 0,
14063
+ skippedLineCount: jsonHistory.skippedMessageCount,
14064
+ sessionJsonMessageCount: jsonHistory.messages.length,
14065
+ jsonlMessageCount: jsonlHistory.messages.length
14066
+ };
14067
+ }
14068
+ return {
14069
+ ...jsonlHistory,
14070
+ sessionJsonMessageCount: jsonHistory.messages.length,
14071
+ jsonlMessageCount: jsonlHistory.messages.length
14072
+ };
14073
+ }
14074
+ async function readFirstExistingFile(paths) {
14075
+ for (const filePath of paths) {
14076
+ const raw = await readFile10(filePath, "utf8").catch((error) => {
14077
+ if (isNodeError12(error, "ENOENT")) {
14078
+ return null;
14079
+ }
14080
+ throw error;
14081
+ });
14082
+ if (raw !== null) {
14083
+ return raw;
14084
+ }
14085
+ }
14086
+ return "";
14087
+ }
14088
+ function candidateTranscriptPaths(sessionsDir, sessionId, extension) {
14089
+ return [
14090
+ path17.join(sessionsDir, `session_${sessionId}.${extension}`),
14091
+ path17.join(sessionsDir, `${sessionId}.${extension}`)
14092
+ ];
14093
+ }
13485
14094
  function readHistoryRows(dbPath, sessionId) {
13486
14095
  let db = null;
13487
14096
  try {
@@ -13534,6 +14143,8 @@ function createHistoryDiagnostics(messages, sourceReason, counts = {}) {
13534
14143
  return {
13535
14144
  source_reason: sourceReason,
13536
14145
  state_db_message_count: counts.state_db_message_count ?? 0,
14146
+ transcript_message_count: counts.transcript_message_count ?? 0,
14147
+ session_json_message_count: counts.session_json_message_count ?? 0,
13537
14148
  jsonl_message_count: counts.jsonl_message_count ?? 0,
13538
14149
  snapshot_message_count: counts.snapshot_message_count ?? 0,
13539
14150
  selected_message_count: messages.length,
@@ -13550,6 +14161,8 @@ function createHistoryDiagnostics(messages, sourceReason, counts = {}) {
13550
14161
  function pickHermesDiagnosticCounts(diagnostics) {
13551
14162
  return {
13552
14163
  state_db_message_count: diagnostics.state_db_message_count,
14164
+ transcript_message_count: diagnostics.transcript_message_count,
14165
+ session_json_message_count: diagnostics.session_json_message_count,
13553
14166
  jsonl_message_count: diagnostics.jsonl_message_count,
13554
14167
  jsonl_byte_count: diagnostics.jsonl_byte_count,
13555
14168
  jsonl_line_count: diagnostics.jsonl_line_count,
@@ -13558,6 +14171,9 @@ function pickHermesDiagnosticCounts(diagnostics) {
13558
14171
  jsonl_sessions_dir_config_error: diagnostics.jsonl_sessions_dir_config_error
13559
14172
  };
13560
14173
  }
14174
+ function isRecord2(value) {
14175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14176
+ }
13561
14177
  function countReplayMetadata(messages) {
13562
14178
  let toolMessageCount = 0;
13563
14179
  let toolCallMessageCount = 0;
@@ -13972,24 +14588,29 @@ async function* parseSseResponse(response) {
13972
14588
  let buffer = "";
13973
14589
  for await (const chunk of response.body) {
13974
14590
  buffer += decoder.decode(chunk, { stream: true });
13975
- let separatorIndex = buffer.indexOf("\n\n");
13976
- while (separatorIndex >= 0) {
13977
- const block = buffer.slice(0, separatorIndex);
13978
- 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);
13979
14595
  const parsed = parseSseBlock(block);
13980
14596
  if (parsed) {
13981
14597
  yield parsed;
13982
14598
  }
13983
- separatorIndex = buffer.indexOf("\n\n");
14599
+ separator = findSseBlockSeparator(buffer);
13984
14600
  }
13985
14601
  }
14602
+ buffer += decoder.decode();
13986
14603
  const trailing = parseSseBlock(buffer);
13987
14604
  if (trailing) {
13988
14605
  yield trailing;
13989
14606
  }
13990
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
+ }
13991
14612
  function parseSseBlock(block) {
13992
- const lines = block.split("\n");
14613
+ const lines = block.split(/\r\n|\n|\r/u);
13993
14614
  let eventName = "";
13994
14615
  const data = [];
13995
14616
  for (const rawLine of lines) {
@@ -14049,7 +14670,7 @@ function resolveConversationRunBackend(env = process.env) {
14049
14670
  if (RUNS_BACKEND_VALUES.has(raw)) {
14050
14671
  return "runs";
14051
14672
  }
14052
- return "runs";
14673
+ return "responses";
14053
14674
  }
14054
14675
  function isRunToolResultCompensationEnabled(env = process.env) {
14055
14676
  const raw = env.HERMESLINK_RUN_TOOL_RESULT_COMPENSATION?.trim().toLowerCase();
@@ -15052,7 +15673,14 @@ var ConversationRunLifecycle = class {
15052
15673
  runId,
15053
15674
  hermesSessionId
15054
15675
  );
15055
- 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({
15056
15684
  paths: this.deps.paths,
15057
15685
  profileName: run.profile,
15058
15686
  hermesSessionId,
@@ -15069,14 +15697,22 @@ var ConversationRunLifecycle = class {
15069
15697
  source: "empty",
15070
15698
  diagnostics: emptyConversationHistoryDiagnostics("build_failed")
15071
15699
  };
15072
- });
15073
- await this.deps.logger.debug("conversation_history_built", {
15074
- conversation_id: conversationId,
15075
- run_id: runId,
15076
- source: conversationHistory.source,
15077
- message_count: conversationHistory.messages.length,
15078
- ...conversationHistory.diagnostics
15079
- });
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
+ );
15080
15716
  const cronJobIdsBeforeRun = await this.readHermesCronJobIds(
15081
15717
  run.profile
15082
15718
  ).catch(() => null);
@@ -15086,12 +15722,6 @@ var ConversationRunLifecycle = class {
15086
15722
  fallbackInput: input,
15087
15723
  snapshot
15088
15724
  });
15089
- const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
15090
- if (previousResponseId) {
15091
- await this.updateRun(conversationId, runId, {
15092
- previous_response_id: previousResponseId
15093
- });
15094
- }
15095
15725
  const deliveryStagingDir = await prepareDeliveryStagingRunDir(
15096
15726
  this.deps.paths,
15097
15727
  conversationId,
@@ -15105,12 +15735,12 @@ var ConversationRunLifecycle = class {
15105
15735
  return void 0;
15106
15736
  });
15107
15737
  const instructions = buildRunInstructions(run, deliveryStagingDir);
15108
- const estimatedUsage = estimateContextUsage({
15738
+ const estimatedUsage = shouldBuildConversationHistory ? estimateContextUsage({
15109
15739
  conversationHistory: conversationHistory.messages,
15110
15740
  currentInput: resolvedInput,
15111
15741
  instructions,
15112
15742
  contextWindow: run.context_window
15113
- });
15743
+ }) : void 0;
15114
15744
  if (estimatedUsage) {
15115
15745
  await this.updateRun(conversationId, runId, { usage: estimatedUsage });
15116
15746
  }
@@ -15119,22 +15749,89 @@ var ConversationRunLifecycle = class {
15119
15749
  run.profile ?? "default"
15120
15750
  );
15121
15751
  if (backend === "responses") {
15122
- const response = await streamHermesResponses(
15123
- {
15124
- input: resolvedInput,
15125
- instructions,
15126
- session_id: hermesSessionId,
15127
- session_key: sessionKey,
15128
- model: run.model,
15129
- ...previousResponseId ? { previous_response_id: previousResponseId } : {},
15130
- ...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
15131
- },
15132
- {
15133
- logger: this.deps.logger,
15134
- profileName: run.profile,
15135
- 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;
15136
15773
  }
15137
- );
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
+ }
15138
15835
  const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
15139
15836
  if (responseSessionId) {
15140
15837
  await this.rememberRunHermesSessionId(
@@ -15230,6 +15927,7 @@ var ConversationRunLifecycle = class {
15230
15927
  );
15231
15928
  }
15232
15929
  async failRun(conversationId, runId, message, source) {
15930
+ await this.refreshRunHermesCompressionTip(conversationId, runId);
15233
15931
  return this.deps.withConversationLock(
15234
15932
  conversationId,
15235
15933
  () => this.failRunLocked(conversationId, runId, message, source)
@@ -15327,9 +16025,21 @@ var ConversationRunLifecycle = class {
15327
16025
  await this.cancelRunAfterAbort(input.conversationId, input.runId);
15328
16026
  return;
15329
16027
  }
15330
- 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) {
15331
16033
  await this.completeRun(input.conversationId, input.runId);
15332
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
+ });
15333
16043
  await this.failRun(
15334
16044
  input.conversationId,
15335
16045
  input.runId,
@@ -15947,11 +16657,38 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
15947
16657
  return user ? messageRequestsAppDelivery(messageText(user)) : false;
15948
16658
  }
15949
16659
  async completeRun(conversationId, runId, source) {
16660
+ await this.refreshRunHermesCompressionTip(conversationId, runId);
15950
16661
  return this.deps.withConversationLock(
15951
16662
  conversationId,
15952
16663
  () => this.completeRunLocked(conversationId, runId, source)
15953
16664
  );
15954
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
+ }
15955
16692
  async completeRunLocked(conversationId, runId, source) {
15956
16693
  let snapshot = await this.deps.readSnapshot(conversationId);
15957
16694
  let run = snapshot.runs.find((item) => item.id === runId);
@@ -16638,6 +17375,7 @@ var ConversationService = class {
16638
17375
  metadata: this.metadata,
16639
17376
  commandHandlers: this.commandHandlers,
16640
17377
  runLifecycle: this.runLifecycle,
17378
+ logger: this.logger,
16641
17379
  withConversationLock: (conversationId, task) => this.withConversationLock(conversationId, task),
16642
17380
  appendEvent: (conversationId, input) => this.appendEvent(conversationId, input),
16643
17381
  resolveMessageAttachmentParts: (conversationId, attachments) => this.maintenance.resolveMessageAttachmentParts(
@@ -17156,7 +17894,14 @@ var ConversationService = class {
17156
17894
  reason: "cancelled by app"
17157
17895
  });
17158
17896
  if (result.run.status === "cancelled") {
17159
- 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
+ );
17160
17905
  }
17161
17906
  return result;
17162
17907
  }
@@ -18176,6 +18921,7 @@ function isLanHost(hostname) {
18176
18921
  // src/http/sse.ts
18177
18922
  var DEFAULT_SSE_RETRY_MS = 1e3;
18178
18923
  var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
18924
+ var activeSseSockets = /* @__PURE__ */ new WeakSet();
18179
18925
  function beginSseStream(request, response, options = {}) {
18180
18926
  const retryMs = normalizeRetryMs(options.retryMs);
18181
18927
  const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
@@ -18183,11 +18929,15 @@ function beginSseStream(request, response, options = {}) {
18183
18929
  response.setHeader("content-type", "text/event-stream; charset=utf-8");
18184
18930
  response.setHeader("cache-control", "no-store");
18185
18931
  response.setHeader("connection", "keep-alive");
18932
+ activeSseSockets.add(request.socket);
18186
18933
  response.flushHeaders();
18187
18934
  writeSseRetry(response, retryMs);
18188
18935
  writeSseComment(response, options.initialComment ?? "connected");
18189
18936
  let closed = false;
18190
18937
  let heartbeat = null;
18938
+ const onStreamError = () => {
18939
+ cleanup();
18940
+ };
18191
18941
  const cleanup = () => {
18192
18942
  if (closed) {
18193
18943
  return;
@@ -18199,9 +18949,18 @@ function beginSseStream(request, response, options = {}) {
18199
18949
  }
18200
18950
  request.off("close", cleanup);
18201
18951
  response.off("close", cleanup);
18952
+ request.off("error", onStreamError);
18953
+ response.off("error", onStreamError);
18954
+ activeSseSockets.delete(request.socket);
18202
18955
  options.onClose?.();
18203
18956
  if (!response.writableEnded && !response.destroyed) {
18204
- response.end();
18957
+ try {
18958
+ response.end();
18959
+ } catch (error) {
18960
+ if (!isExpectedClientDisconnectError(error)) {
18961
+ throw error;
18962
+ }
18963
+ }
18205
18964
  }
18206
18965
  };
18207
18966
  heartbeat = setInterval(() => {
@@ -18214,37 +18973,43 @@ function beginSseStream(request, response, options = {}) {
18214
18973
  heartbeat.unref();
18215
18974
  request.once("close", cleanup);
18216
18975
  response.once("close", cleanup);
18976
+ request.once("error", onStreamError);
18977
+ response.once("error", onStreamError);
18217
18978
  return cleanup;
18218
18979
  }
18980
+ function isActiveSseSocket(socket) {
18981
+ return socket != null && activeSseSockets.has(socket);
18982
+ }
18219
18983
  function writeSseEvent(response, event) {
18984
+ const appEvent = projectAppConversationEvent(event);
18220
18985
  writeJsonSseEvent(response, {
18221
- event: event.type,
18222
- data: event,
18223
- id: event.seq
18986
+ event: appEvent.type,
18987
+ data: appEvent,
18988
+ id: appEvent.seq
18224
18989
  });
18225
18990
  }
18226
18991
  function writeJsonSseEvent(response, event) {
18227
18992
  if (event.retryMs != null) {
18228
- response.write(`retry: ${normalizeRetryMs(event.retryMs)}
18993
+ writeResponse(response, `retry: ${normalizeRetryMs(event.retryMs)}
18229
18994
  `);
18230
18995
  }
18231
18996
  if (event.id != null && event.id !== "") {
18232
- response.write(`id: ${event.id}
18997
+ writeResponse(response, `id: ${event.id}
18233
18998
  `);
18234
18999
  }
18235
- response.write(`event: ${event.event}
19000
+ writeResponse(response, `event: ${event.event}
18236
19001
  `);
18237
- response.write(`data: ${JSON.stringify(event.data)}
19002
+ writeResponse(response, `data: ${JSON.stringify(event.data)}
18238
19003
 
18239
19004
  `);
18240
19005
  }
18241
19006
  function writeSseComment(response, comment = "keep-alive") {
18242
- response.write(`: ${comment}
19007
+ writeResponse(response, `: ${comment}
18243
19008
 
18244
19009
  `);
18245
19010
  }
18246
19011
  function writeSseRetry(response, retryMs) {
18247
- response.write(`retry: ${normalizeRetryMs(retryMs)}
19012
+ writeResponse(response, `retry: ${normalizeRetryMs(retryMs)}
18248
19013
 
18249
19014
  `);
18250
19015
  }
@@ -18252,6 +19017,25 @@ function normalizeRetryMs(retryMs) {
18252
19017
  const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
18253
19018
  return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
18254
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
+ }
18255
19039
 
18256
19040
  // src/http/routes/conversations.ts
18257
19041
  function registerConversationRoutes(router, options) {
@@ -18732,12 +19516,22 @@ function encodeRfc5987Value(value) {
18732
19516
  }
18733
19517
 
18734
19518
  // src/http/middleware/error-handler.ts
19519
+ var INTERNAL_HEALTH_PROBE_HEADER = "x-hermes-link-internal-health-probe";
18735
19520
  function createHttpErrorMiddleware(logger) {
18736
19521
  return async (ctx, next) => {
18737
19522
  const startedAt = Date.now();
19523
+ const shouldSkipRequestLog = isInternalHealthProbe(ctx);
19524
+ let expectedClientDisconnect = false;
18738
19525
  try {
18739
19526
  await next();
18740
19527
  } catch (error) {
19528
+ if (isExpectedClientDisconnectError2(error, {
19529
+ sse: isSseRequestContext(ctx)
19530
+ })) {
19531
+ expectedClientDisconnect = true;
19532
+ ctx.respond = false;
19533
+ return;
19534
+ }
18741
19535
  const profileError = error instanceof Error && error.message === "invalid profile name";
18742
19536
  const profileNotFound = error instanceof Error && error.message === "profile does not exist";
18743
19537
  const status = isLinkHttpError(error) ? error.status : profileError ? 400 : profileNotFound ? 404 : 500;
@@ -18750,28 +19544,57 @@ function createHttpErrorMiddleware(logger) {
18750
19544
  message: error instanceof Error ? error.message : "Internal error"
18751
19545
  }
18752
19546
  };
18753
- void logger.write(
18754
- status >= 500 ? "error" : "warn",
18755
- "http_request_failed",
18756
- {
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", {
18757
19564
  method: ctx.method,
18758
19565
  path: ctx.path,
18759
- query: ctx.querystring || null,
18760
- status,
18761
- code,
18762
- error: error instanceof Error ? error.message : String(error)
18763
- }
18764
- );
18765
- } finally {
18766
- void logger.info("http_request", {
18767
- method: ctx.method,
18768
- path: ctx.path,
18769
- status: ctx.status,
18770
- duration_ms: Date.now() - startedAt
18771
- });
19566
+ status: ctx.status,
19567
+ duration_ms: Date.now() - startedAt
19568
+ });
19569
+ }
18772
19570
  }
18773
19571
  };
18774
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
+ }
18775
19598
 
18776
19599
  // src/hermes/profiles.ts
18777
19600
  import { execFile as execFile4 } from "child_process";
@@ -19749,7 +20572,12 @@ function readModelConfigInput(body) {
19749
20572
  function readModelDefaultsInput(body) {
19750
20573
  return {
19751
20574
  taskModelId: readString16(body, "task_model_id") ?? readString16(body, "taskModelId") ?? readString16(body, "default_model_id") ?? readString16(body, "defaultModelId") ?? void 0,
19752
- 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
19753
20581
  };
19754
20582
  }
19755
20583
  function readModelConfigImportInput(body) {
@@ -19852,41 +20680,348 @@ import { EventEmitter as EventEmitter2 } from "events";
19852
20680
  import {
19853
20681
  cp,
19854
20682
  mkdir as mkdir11,
19855
- readFile as readFile14,
20683
+ readFile as readFile15,
19856
20684
  rm as rm6,
19857
- stat as stat14
20685
+ stat as stat15
19858
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";
19859
20693
  import path21 from "path";
19860
20694
  import YAML3 from "yaml";
19861
- var PROFILE_CREATE_LOG_FILE = "profile-create.log";
19862
- var PROFILE_CREATE_LOG_MAX_FILES = 3;
19863
- var MAX_PROFILE_CREATE_LOG_LINES = 260;
19864
- var MAX_OUTPUT_LINE_LENGTH = 1200;
19865
- var PROFILE_NAME_PATTERN5 = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
19866
- var ALL_COPY_SCOPES = [
19867
- "models",
19868
- "skills",
19869
- "tool_permissions",
19870
- "approval_policy"
19871
- ];
19872
- var PROFILE_CREATION_EVENTS = new EventEmitter2();
19873
- var runningProfileCreation = null;
19874
- async function startHermesProfileCreation(input, options) {
19875
- const current = await readHermesProfileCreationStatus(options.paths);
19876
- if (runningProfileCreation || current.state === "running") {
19877
- 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
+ }
19878
20801
  }
19879
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
19880
- const normalized = await normalizeProfileCreationInput(input, options.paths);
19881
- const profileName = normalized.name ?? await generateProfileName(normalized.displayName, options.paths);
19882
- const sourceProfile = normalized.copyFrom;
19883
- const copyScopes = normalized.copyScopes;
19884
- const jobId = `profile_create_${now().getTime().toString(36)}`;
19885
- await clearProfileCreationLogFiles(options.paths);
19886
- const writer = createRotatingTextLogWriter({
19887
- paths: options.paths,
19888
- fileName: PROFILE_CREATE_LOG_FILE,
19889
- maxFileBytes: 512 * 1024,
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
20828
+ };
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
+ });
20838
+ }
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;
20849
+ });
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,
19890
21025
  maxFiles: PROFILE_CREATE_LOG_MAX_FILES
19891
21026
  });
19892
21027
  const startedAt = now().toISOString();
@@ -20194,6 +21329,11 @@ async function applyProfileCreationPostSteps(input) {
20194
21329
  await input.writer.write("Ensuring Hermes API Server config...\n");
20195
21330
  await ensureHermesApiServerKey(input.profileName);
20196
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
+ });
20197
21337
  if (input.displayName || input.description || input.avatarType === "url" || input.avatarUrl) {
20198
21338
  await input.writer.write("Saving Profile display metadata...\n");
20199
21339
  await updateProfileMetadata(input.paths, {
@@ -20246,23 +21386,23 @@ function copyModelConfig(source, target) {
20246
21386
  copied[key] = cloneJson(source[key]);
20247
21387
  }
20248
21388
  }
20249
- const sourceAuxiliary = toRecord15(source.auxiliary);
21389
+ const sourceAuxiliary = toRecord16(source.auxiliary);
20250
21390
  if (Object.prototype.hasOwnProperty.call(sourceAuxiliary, "compression")) {
20251
- const targetAuxiliary = ensureRecord2(target, "auxiliary");
21391
+ const targetAuxiliary = ensureRecord3(target, "auxiliary");
20252
21392
  targetAuxiliary.compression = cloneJson(sourceAuxiliary.compression);
20253
21393
  copied.auxiliary = { compression: cloneJson(sourceAuxiliary.compression) };
20254
21394
  }
20255
21395
  return copied;
20256
21396
  }
20257
21397
  function copyToolPermissionsConfig(source, target) {
20258
- const sourcePlatformToolsets = toRecord15(source.platform_toolsets);
21398
+ const sourcePlatformToolsets = toRecord16(source.platform_toolsets);
20259
21399
  if (Object.prototype.hasOwnProperty.call(sourcePlatformToolsets, "api_server")) {
20260
- const targetPlatformToolsets = ensureRecord2(target, "platform_toolsets");
21400
+ const targetPlatformToolsets = ensureRecord3(target, "platform_toolsets");
20261
21401
  targetPlatformToolsets.api_server = cloneJson(sourcePlatformToolsets.api_server);
20262
21402
  }
20263
- const sourceStt = toRecord15(source.stt);
21403
+ const sourceStt = toRecord16(source.stt);
20264
21404
  if (Object.prototype.hasOwnProperty.call(sourceStt, "enabled")) {
20265
- const targetStt = ensureRecord2(target, "stt");
21405
+ const targetStt = ensureRecord3(target, "stt");
20266
21406
  targetStt.enabled = cloneJson(sourceStt.enabled);
20267
21407
  }
20268
21408
  copyProperty(source, target, "command_allowlist");
@@ -20307,9 +21447,9 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
20307
21447
  return keys;
20308
21448
  }
20309
21449
  async function writeEnvValues(profileName, values) {
20310
- const envPath = path21.join(resolveHermesProfileDir(profileName), ".env");
20311
- const existingRaw = await readFile14(envPath, "utf8").catch((error) => {
20312
- 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")) {
20313
21453
  return "";
20314
21454
  }
20315
21455
  throw error;
@@ -20344,8 +21484,8 @@ async function writeEnvValues(profileName, values) {
20344
21484
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
20345
21485
  }
20346
21486
  async function copySkills(sourceProfile, targetProfile) {
20347
- const sourceSkills = path21.join(resolveHermesProfileDir(sourceProfile), "skills");
20348
- const targetSkills = path21.join(resolveHermesProfileDir(targetProfile), "skills");
21487
+ const sourceSkills = path22.join(resolveHermesProfileDir(sourceProfile), "skills");
21488
+ const targetSkills = path22.join(resolveHermesProfileDir(targetProfile), "skills");
20349
21489
  if (!await pathExists2(sourceSkills)) {
20350
21490
  return;
20351
21491
  }
@@ -20368,16 +21508,16 @@ function copyProperty(source, target, key) {
20368
21508
  }
20369
21509
  }
20370
21510
  async function readYamlConfig(configPath) {
20371
- const existingRaw = await readFile14(configPath, "utf8").catch(
21511
+ const existingRaw = await readFile15(configPath, "utf8").catch(
20372
21512
  (error) => {
20373
- if (isNodeError16(error, "ENOENT")) {
21513
+ if (isNodeError17(error, "ENOENT")) {
20374
21514
  return null;
20375
21515
  }
20376
21516
  throw error;
20377
21517
  }
20378
21518
  );
20379
21519
  return {
20380
- config: toRecord15(existingRaw ? YAML3.parse(existingRaw) : {}),
21520
+ config: toRecord16(existingRaw ? YAML4.parse(existingRaw) : {}),
20381
21521
  existingRaw
20382
21522
  };
20383
21523
  }
@@ -20389,7 +21529,7 @@ async function writeYamlConfig(configPath, input) {
20389
21529
  { metadataSourcePath: configPath }
20390
21530
  );
20391
21531
  }
20392
- const document = new YAML3.Document(input.config);
21532
+ const document = new YAML4.Document(input.config);
20393
21533
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
20394
21534
  }
20395
21535
  async function failProfileCreation(input) {
@@ -20431,7 +21571,7 @@ async function writeProfileCreationState(paths, state) {
20431
21571
  await writeJsonFile(profileCreationStatePath(paths), state);
20432
21572
  }
20433
21573
  async function readProfileCreationLogLines(paths) {
20434
- const raw = await readFile14(profileCreationLogPath(paths), "utf8").catch(() => "");
21574
+ const raw = await readFile15(profileCreationLogPath(paths), "utf8").catch(() => "");
20435
21575
  if (!raw.trim()) {
20436
21576
  return [];
20437
21577
  }
@@ -20440,10 +21580,10 @@ async function readProfileCreationLogLines(paths) {
20440
21580
  );
20441
21581
  }
20442
21582
  function profileCreationStatePath(paths) {
20443
- return path21.join(paths.runDir, "profile-create-state.json");
21583
+ return path22.join(paths.runDir, "profile-create-state.json");
20444
21584
  }
20445
21585
  function profileCreationLogPath(paths) {
20446
- return path21.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
21586
+ return path22.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
20447
21587
  }
20448
21588
  async function clearProfileCreationLogFiles(paths) {
20449
21589
  const primary = profileCreationLogPath(paths);
@@ -20456,8 +21596,8 @@ async function clearProfileCreationLogFiles(paths) {
20456
21596
  ]);
20457
21597
  }
20458
21598
  async function pathExists2(targetPath) {
20459
- return await stat14(targetPath).then(() => true).catch((error) => {
20460
- if (isNodeError16(error, "ENOENT")) {
21599
+ return await stat15(targetPath).then(() => true).catch((error) => {
21600
+ if (isNodeError17(error, "ENOENT")) {
20461
21601
  return false;
20462
21602
  }
20463
21603
  throw error;
@@ -20480,7 +21620,7 @@ function isProcessAlive(pid) {
20480
21620
  return false;
20481
21621
  }
20482
21622
  }
20483
- function ensureRecord2(target, key) {
21623
+ function ensureRecord3(target, key) {
20484
21624
  const value = target[key];
20485
21625
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
20486
21626
  return value;
@@ -20489,7 +21629,7 @@ function ensureRecord2(target, key) {
20489
21629
  target[key] = next;
20490
21630
  return next;
20491
21631
  }
20492
- function toRecord15(value) {
21632
+ function toRecord16(value) {
20493
21633
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
20494
21634
  }
20495
21635
  function cloneJson(value) {
@@ -20504,7 +21644,7 @@ function formatEnvValue2(value) {
20504
21644
  function escapeRegExp3(value) {
20505
21645
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
20506
21646
  }
20507
- function isNodeError16(error, code) {
21647
+ function isNodeError17(error, code) {
20508
21648
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
20509
21649
  }
20510
21650
 
@@ -20714,11 +21854,11 @@ function toProfileToolConfigHttpError(error) {
20714
21854
  import {
20715
21855
  access as access3,
20716
21856
  readdir as readdir10,
20717
- readFile as readFile15,
20718
- stat as stat15
21857
+ readFile as readFile16,
21858
+ stat as stat16
20719
21859
  } from "fs/promises";
20720
- import path22 from "path";
20721
- import YAML4 from "yaml";
21860
+ import path23 from "path";
21861
+ import YAML5 from "yaml";
20722
21862
  var ENTRY_DELIMITER = "\n\xA7\n";
20723
21863
  var DEFAULT_MEMORY_LIMIT = 2200;
20724
21864
  var DEFAULT_USER_LIMIT = 1375;
@@ -21039,7 +22179,7 @@ async function saveProviderSettings(profileName, provider, patch) {
21039
22179
  });
21040
22180
  await patchJsonProviderConfig(
21041
22181
  profileName,
21042
- path22.join("hindsight", "config.json"),
22182
+ path23.join("hindsight", "config.json"),
21043
22183
  {
21044
22184
  mode: patch.mode,
21045
22185
  api_url: patch.apiUrl,
@@ -21096,7 +22236,7 @@ async function patchCustomProviderConfig(profileName, provider, patch) {
21096
22236
  "\u81EA\u5B9A\u4E49 memory provider \u914D\u7F6E\u5FC5\u987B\u662F\u6709\u6548\u7684 JSON object\u3002"
21097
22237
  );
21098
22238
  }
21099
- const config = toRecord16(parsed);
22239
+ const config = toRecord17(parsed);
21100
22240
  if (Object.keys(config).length === 0 && parsed !== null) {
21101
22241
  throw new HermesMemoryError(
21102
22242
  "memory_provider_config_invalid",
@@ -21187,17 +22327,17 @@ function normalizeCustomProviderId(provider) {
21187
22327
  }
21188
22328
  async function patchHermesMemoryProvider(profileName, provider) {
21189
22329
  const configPath = resolveHermesConfigPath(profileName);
21190
- const existingRaw = await readFile15(configPath, "utf8").catch(
22330
+ const existingRaw = await readFile16(configPath, "utf8").catch(
21191
22331
  (error) => {
21192
- if (isNodeError17(error, "ENOENT")) {
22332
+ if (isNodeError18(error, "ENOENT")) {
21193
22333
  return null;
21194
22334
  }
21195
22335
  throw error;
21196
22336
  }
21197
22337
  );
21198
- const document = existingRaw ? YAML4.parseDocument(existingRaw) : new YAML4.Document({});
21199
- const config = toRecord16(document.toJSON());
21200
- 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);
21201
22341
  memory.provider = provider === "built-in" ? "" : provider;
21202
22342
  config.memory = memory;
21203
22343
  const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
@@ -21210,13 +22350,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
21210
22350
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
21211
22351
  }
21212
22352
  function resolveMemoryDir(profileName) {
21213
- return path22.join(resolveHermesProfileDir(profileName), "memories");
22353
+ return path23.join(resolveHermesProfileDir(profileName), "memories");
21214
22354
  }
21215
22355
  async function readMemoryStore(profileName, target, limits) {
21216
22356
  const filePath = memoryFilePath(profileName, target);
21217
22357
  const entries = await readMemoryEntries(filePath);
21218
- const fileStat = await stat15(filePath).catch((error) => {
21219
- if (isNodeError17(error, "ENOENT")) {
22358
+ const fileStat = await stat16(filePath).catch((error) => {
22359
+ if (isNodeError18(error, "ENOENT")) {
21220
22360
  return null;
21221
22361
  }
21222
22362
  throw error;
@@ -21244,8 +22384,8 @@ async function readMemoryStore(profileName, target, limits) {
21244
22384
  };
21245
22385
  }
21246
22386
  async function readMemoryEntries(filePath) {
21247
- const raw = await readFile15(filePath, "utf8").catch((error) => {
21248
- if (isNodeError17(error, "ENOENT")) {
22387
+ const raw = await readFile16(filePath, "utf8").catch((error) => {
22388
+ if (isNodeError18(error, "ENOENT")) {
21249
22389
  return "";
21250
22390
  }
21251
22391
  throw error;
@@ -21271,7 +22411,7 @@ async function writeMemoryEntries(profileName, target, entries) {
21271
22411
  );
21272
22412
  }
21273
22413
  function memoryFilePath(profileName, target) {
21274
- return path22.join(
22414
+ return path23.join(
21275
22415
  resolveMemoryDir(profileName),
21276
22416
  target === "user" ? "USER.md" : "MEMORY.md"
21277
22417
  );
@@ -21304,7 +22444,7 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
21304
22444
  }
21305
22445
  return Promise.all(
21306
22446
  [...descriptors.values()].map(async (descriptor) => {
21307
- const installed = await isUserMemoryProviderInstalled(
22447
+ const installed2 = await isUserMemoryProviderInstalled(
21308
22448
  profileName,
21309
22449
  descriptor.id
21310
22450
  );
@@ -21314,8 +22454,8 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
21314
22454
  description: descriptor.description,
21315
22455
  active: descriptor.id === activeProviderId,
21316
22456
  configurable: true,
21317
- configured: installed,
21318
- 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",
21319
22459
  providerConfigPath: customProviderConfigPath(profileName, descriptor.id),
21320
22460
  settings: await readCustomProviderSettings(profileName, descriptor.id)
21321
22461
  };
@@ -21331,7 +22471,7 @@ async function readCustomProviderSetupSummary(profileName) {
21331
22471
  configurable: true,
21332
22472
  configured: true,
21333
22473
  configurationIssue: null,
21334
- providerConfigPath: path22.join(
22474
+ providerConfigPath: path23.join(
21335
22475
  resolveHermesProfileDir(profileName),
21336
22476
  "<provider>.json"
21337
22477
  ),
@@ -21660,8 +22800,8 @@ async function readProviderSettings(profileName, provider) {
21660
22800
  memoryProviderConfigPath(profileName, provider) ?? ""
21661
22801
  );
21662
22802
  const env = await readHermesMemoryEnv(profileName);
21663
- const banks = toRecord16(config.banks);
21664
- const hermesBank = toRecord16(banks.hermes);
22803
+ const banks = toRecord17(config.banks);
22804
+ const hermesBank = toRecord17(banks.hermes);
21665
22805
  const mode = normalizeHindsightMode(config.mode);
21666
22806
  return [
21667
22807
  selectSetting("mode", "\u8FDE\u63A5\u6A21\u5F0F", mode, [
@@ -21725,7 +22865,7 @@ async function readProviderSettings(profileName, provider) {
21725
22865
  stringSetting(
21726
22866
  "dbPath",
21727
22867
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
21728
- config.db_path ?? path22.join(resolveHermesProfileDir(profileName), "memory_store.db")
22868
+ config.db_path ?? path23.join(resolveHermesProfileDir(profileName), "memory_store.db")
21729
22869
  ),
21730
22870
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
21731
22871
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -21761,7 +22901,7 @@ async function readProviderSettings(profileName, provider) {
21761
22901
  stringSetting(
21762
22902
  "workingDirectory",
21763
22903
  "\u5DE5\u4F5C\u76EE\u5F55",
21764
- path22.join(resolveHermesProfileDir(profileName), "byterover"),
22904
+ path23.join(resolveHermesProfileDir(profileName), "byterover"),
21765
22905
  false
21766
22906
  )
21767
22907
  ];
@@ -21770,16 +22910,16 @@ async function readProviderSettings(profileName, provider) {
21770
22910
  }
21771
22911
  function memoryProviderConfigPath(profileName, provider) {
21772
22912
  if (provider === "honcho") {
21773
- return path22.join(resolveHermesProfileDir(profileName), "honcho.json");
22913
+ return path23.join(resolveHermesProfileDir(profileName), "honcho.json");
21774
22914
  }
21775
22915
  if (provider === "mem0") {
21776
- return path22.join(resolveHermesProfileDir(profileName), "mem0.json");
22916
+ return path23.join(resolveHermesProfileDir(profileName), "mem0.json");
21777
22917
  }
21778
22918
  if (provider === "supermemory") {
21779
- return path22.join(resolveHermesProfileDir(profileName), "supermemory.json");
22919
+ return path23.join(resolveHermesProfileDir(profileName), "supermemory.json");
21780
22920
  }
21781
22921
  if (provider === "hindsight") {
21782
- return path22.join(
22922
+ return path23.join(
21783
22923
  resolveHermesProfileDir(profileName),
21784
22924
  "hindsight",
21785
22925
  "config.json"
@@ -21788,21 +22928,21 @@ function memoryProviderConfigPath(profileName, provider) {
21788
22928
  return null;
21789
22929
  }
21790
22930
  function customProviderConfigPath(profileName, provider) {
21791
- return path22.join(
22931
+ return path23.join(
21792
22932
  resolveHermesProfileDir(profileName),
21793
22933
  `${normalizeCustomProviderId(provider)}.json`
21794
22934
  );
21795
22935
  }
21796
22936
  function customProviderRegistryPath(profileName) {
21797
- return path22.join(
22937
+ return path23.join(
21798
22938
  resolveHermesProfileDir(profileName),
21799
22939
  CUSTOM_PROVIDER_REGISTRY_FILE
21800
22940
  );
21801
22941
  }
21802
22942
  async function readCustomProviderRegistry(profileName) {
21803
- const raw = await readFile15(customProviderRegistryPath(profileName), "utf8").catch(
22943
+ const raw = await readFile16(customProviderRegistryPath(profileName), "utf8").catch(
21804
22944
  (error) => {
21805
- if (isNodeError17(error, "ENOENT")) {
22945
+ if (isNodeError18(error, "ENOENT")) {
21806
22946
  return "";
21807
22947
  }
21808
22948
  throw error;
@@ -21813,13 +22953,13 @@ async function readCustomProviderRegistry(profileName) {
21813
22953
  }
21814
22954
  try {
21815
22955
  const parsed = JSON.parse(raw);
21816
- 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 : [];
21817
22957
  return providers.map((item) => {
21818
22958
  if (typeof item === "string") {
21819
22959
  const id2 = normalizeCustomProviderId(item);
21820
22960
  return { id: id2, label: id2, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" };
21821
22961
  }
21822
- const record = toRecord16(item);
22962
+ const record = toRecord17(item);
21823
22963
  const id = normalizeCustomProviderId(readString17(record.id) ?? "");
21824
22964
  return {
21825
22965
  id,
@@ -21846,10 +22986,10 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
21846
22986
  );
21847
22987
  }
21848
22988
  async function discoverUserMemoryProviderDescriptors(profileName) {
21849
- const pluginsDir = path22.join(resolveHermesProfileDir(profileName), "plugins");
22989
+ const pluginsDir = path23.join(resolveHermesProfileDir(profileName), "plugins");
21850
22990
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
21851
22991
  (error) => {
21852
- if (isNodeError17(error, "ENOENT")) {
22992
+ if (isNodeError18(error, "ENOENT")) {
21853
22993
  return [];
21854
22994
  }
21855
22995
  throw error;
@@ -21866,7 +23006,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
21866
23006
  } catch {
21867
23007
  continue;
21868
23008
  }
21869
- const providerDir = path22.join(pluginsDir, entry.name);
23009
+ const providerDir = path23.join(pluginsDir, entry.name);
21870
23010
  if (!await isMemoryProviderPluginDir(providerDir)) {
21871
23011
  continue;
21872
23012
  }
@@ -21880,7 +23020,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
21880
23020
  return descriptors;
21881
23021
  }
21882
23022
  async function isUserMemoryProviderInstalled(profileName, provider) {
21883
- const providerDir = path22.join(
23023
+ const providerDir = path23.join(
21884
23024
  resolveHermesProfileDir(profileName),
21885
23025
  "plugins",
21886
23026
  normalizeCustomProviderId(provider)
@@ -21888,9 +23028,9 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
21888
23028
  return isMemoryProviderPluginDir(providerDir);
21889
23029
  }
21890
23030
  async function isMemoryProviderPluginDir(providerDir) {
21891
- const source = await readFile15(path22.join(providerDir, "__init__.py"), "utf8").catch(
23031
+ const source = await readFile16(path23.join(providerDir, "__init__.py"), "utf8").catch(
21892
23032
  (error) => {
21893
- if (isNodeError17(error, "ENOENT")) {
23033
+ if (isNodeError18(error, "ENOENT")) {
21894
23034
  return "";
21895
23035
  }
21896
23036
  throw error;
@@ -21900,22 +23040,22 @@ async function isMemoryProviderPluginDir(providerDir) {
21900
23040
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
21901
23041
  }
21902
23042
  async function readPluginMetadata(providerDir) {
21903
- const raw = await readFile15(path22.join(providerDir, "plugin.yaml"), "utf8").catch(
23043
+ const raw = await readFile16(path23.join(providerDir, "plugin.yaml"), "utf8").catch(
21904
23044
  (error) => {
21905
- if (isNodeError17(error, "ENOENT")) {
23045
+ if (isNodeError18(error, "ENOENT")) {
21906
23046
  return "";
21907
23047
  }
21908
23048
  throw error;
21909
23049
  }
21910
23050
  );
21911
- return raw ? toRecord16(YAML4.parse(raw)) : {};
23051
+ return raw ? toRecord17(YAML5.parse(raw)) : {};
21912
23052
  }
21913
23053
  async function resolveByteRoverCli() {
21914
23054
  const candidates = [
21915
- ...(process.env.PATH ?? "").split(path22.delimiter).filter(Boolean).map((dir) => path22.join(dir, "brv")),
21916
- 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"),
21917
23057
  "/usr/local/bin/brv",
21918
- path22.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
23058
+ path23.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
21919
23059
  ].filter(Boolean);
21920
23060
  for (const candidate of candidates) {
21921
23061
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -21926,32 +23066,32 @@ async function resolveByteRoverCli() {
21926
23066
  return null;
21927
23067
  }
21928
23068
  async function readHolographicProviderConfig(profileName) {
21929
- const raw = await readFile15(resolveHermesConfigPath(profileName), "utf8").catch(
23069
+ const raw = await readFile16(resolveHermesConfigPath(profileName), "utf8").catch(
21930
23070
  (error) => {
21931
- if (isNodeError17(error, "ENOENT")) {
23071
+ if (isNodeError18(error, "ENOENT")) {
21932
23072
  return "";
21933
23073
  }
21934
23074
  throw error;
21935
23075
  }
21936
23076
  );
21937
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
21938
- const plugins = toRecord16(config.plugins);
21939
- 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"]);
21940
23080
  }
21941
23081
  async function patchHolographicProviderConfig(profileName, patch) {
21942
23082
  const configPath = resolveHermesConfigPath(profileName);
21943
- const existingRaw = await readFile15(configPath, "utf8").catch(
23083
+ const existingRaw = await readFile16(configPath, "utf8").catch(
21944
23084
  (error) => {
21945
- if (isNodeError17(error, "ENOENT")) {
23085
+ if (isNodeError18(error, "ENOENT")) {
21946
23086
  return null;
21947
23087
  }
21948
23088
  throw error;
21949
23089
  }
21950
23090
  );
21951
- const document = existingRaw ? YAML4.parseDocument(existingRaw) : new YAML4.Document({});
21952
- const config = toRecord16(document.toJSON());
21953
- const plugins = toRecord16(config.plugins);
21954
- 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"]);
21955
23095
  for (const [key, value] of Object.entries(patch)) {
21956
23096
  if (value !== void 0) {
21957
23097
  memoryStore[key] = value;
@@ -21976,9 +23116,9 @@ async function patchHermesMemoryEnv(profileName, patch) {
21976
23116
  if (entries.length === 0) {
21977
23117
  return;
21978
23118
  }
21979
- const envPath = path22.join(resolveHermesProfileDir(profileName), ".env");
21980
- const existingRaw = await readFile15(envPath, "utf8").catch((error) => {
21981
- 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")) {
21982
23122
  return "";
21983
23123
  }
21984
23124
  throw error;
@@ -22099,7 +23239,7 @@ function joinHindsightUrl(baseUrl, pathName) {
22099
23239
  }
22100
23240
  function parseJsonObject2(text) {
22101
23241
  try {
22102
- return toRecord16(JSON.parse(text));
23242
+ return toRecord17(JSON.parse(text));
22103
23243
  } catch {
22104
23244
  return {};
22105
23245
  }
@@ -22129,17 +23269,17 @@ function summarizeHindsightProbe(pathName, json) {
22129
23269
  return bankId ? `bank ${bankId} reachable` : "bank config reachable";
22130
23270
  }
22131
23271
  async function readActiveMemoryProvider(profileName) {
22132
- const raw = await readFile15(
23272
+ const raw = await readFile16(
22133
23273
  resolveHermesConfigPath(profileName),
22134
23274
  "utf8"
22135
23275
  ).catch((error) => {
22136
- if (isNodeError17(error, "ENOENT")) {
23276
+ if (isNodeError18(error, "ENOENT")) {
22137
23277
  return "";
22138
23278
  }
22139
23279
  throw error;
22140
23280
  });
22141
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
22142
- const memory = toRecord16(config.memory);
23281
+ const config = raw ? toRecord17(YAML5.parse(raw)) : {};
23282
+ const memory = toRecord17(config.memory);
22143
23283
  const provider = readString17(memory.provider);
22144
23284
  if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
22145
23285
  return null;
@@ -22147,7 +23287,7 @@ async function readActiveMemoryProvider(profileName) {
22147
23287
  return provider;
22148
23288
  }
22149
23289
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
22150
- const configPath = path22.join(
23290
+ const configPath = path23.join(
22151
23291
  resolveHermesProfileDir(profileName),
22152
23292
  relativePath
22153
23293
  );
@@ -22165,18 +23305,18 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
22165
23305
  );
22166
23306
  }
22167
23307
  async function readJsonObject(filePath) {
22168
- const raw = await readFile15(filePath, "utf8").catch((error) => {
22169
- if (isNodeError17(error, "ENOENT")) {
23308
+ const raw = await readFile16(filePath, "utf8").catch((error) => {
23309
+ if (isNodeError18(error, "ENOENT")) {
22170
23310
  return "{}";
22171
23311
  }
22172
23312
  throw error;
22173
23313
  });
22174
23314
  try {
22175
- return toRecord16(JSON.parse(raw || "{}"));
23315
+ return toRecord17(JSON.parse(raw || "{}"));
22176
23316
  } catch {
22177
23317
  throw new HermesMemoryError(
22178
23318
  "memory_provider_config_invalid",
22179
- `${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`
22180
23320
  );
22181
23321
  }
22182
23322
  }
@@ -22226,17 +23366,17 @@ function selectSetting(key, label, value, options, editable = true) {
22226
23366
  return { key, label, value: stringValue, editable, kind: "select", options };
22227
23367
  }
22228
23368
  async function readMemoryLimits(profileName) {
22229
- const raw = await readFile15(
23369
+ const raw = await readFile16(
22230
23370
  resolveHermesConfigPath(profileName),
22231
23371
  "utf8"
22232
23372
  ).catch((error) => {
22233
- if (isNodeError17(error, "ENOENT")) {
23373
+ if (isNodeError18(error, "ENOENT")) {
22234
23374
  return "";
22235
23375
  }
22236
23376
  throw error;
22237
23377
  });
22238
- const config = raw ? toRecord16(YAML4.parse(raw)) : {};
22239
- const memory = toRecord16(config.memory);
23378
+ const config = raw ? toRecord17(YAML5.parse(raw)) : {};
23379
+ const memory = toRecord17(config.memory);
22240
23380
  return {
22241
23381
  memory: readPositiveInteger3(memory.memory_char_limit) ?? DEFAULT_MEMORY_LIMIT,
22242
23382
  user: readPositiveInteger3(memory.user_char_limit) ?? DEFAULT_USER_LIMIT
@@ -22292,7 +23432,7 @@ function hashString(value) {
22292
23432
  }
22293
23433
  return hash.toString(16);
22294
23434
  }
22295
- function toRecord16(value) {
23435
+ function toRecord17(value) {
22296
23436
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
22297
23437
  }
22298
23438
  function readString17(value) {
@@ -22323,7 +23463,7 @@ function formatEnvValue3(value) {
22323
23463
  function escapeRegExp4(value) {
22324
23464
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
22325
23465
  }
22326
- function isNodeError17(error, code) {
23466
+ function isNodeError18(error, code) {
22327
23467
  return error instanceof Error && "code" in error && error.code === code;
22328
23468
  }
22329
23469
 
@@ -22764,9 +23904,9 @@ function toMemoryHttpError(error) {
22764
23904
  }
22765
23905
 
22766
23906
  // src/hermes/skills.ts
22767
- import { readFile as readFile16, readdir as readdir11 } from "fs/promises";
22768
- import path23 from "path";
22769
- 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";
22770
23910
  var HermesSkillNotFoundError = class extends Error {
22771
23911
  constructor(skillName) {
22772
23912
  super(`skill "${skillName}" does not exist`);
@@ -22779,7 +23919,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
22779
23919
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
22780
23920
  const profile = await readExistingProfile(profileName, paths);
22781
23921
  const profileDir = resolveHermesProfileDir(profile.name);
22782
- const skillsRoot = path23.join(profileDir, "skills");
23922
+ const skillsRoot = path24.join(profileDir, "skills");
22783
23923
  const [skillFiles, disabled, provenance] = await Promise.all([
22784
23924
  findSkillFiles(skillsRoot),
22785
23925
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -22815,8 +23955,8 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
22815
23955
  throw new HermesSkillNotFoundError(skillName);
22816
23956
  }
22817
23957
  const configPath = resolveHermesConfigPath(current.profile.name);
22818
- const { document, config, existingRaw } = await readHermesConfigDocument2(configPath);
22819
- const skillsConfig = ensureRecord3(config, "skills");
23958
+ const { document, config, existingRaw } = await readHermesConfigDocument3(configPath);
23959
+ const skillsConfig = ensureRecord4(config, "skills");
22820
23960
  const disabled = new Set(readStringList3(skillsConfig.disabled));
22821
23961
  if (enabled) {
22822
23962
  disabled.delete(target.name);
@@ -22826,7 +23966,7 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
22826
23966
  skillsConfig.disabled = [...disabled].sort(
22827
23967
  (left, right) => left.localeCompare(right)
22828
23968
  );
22829
- const backupPath = await writeHermesConfigDocument2({
23969
+ const backupPath = await writeHermesConfigDocument3({
22830
23970
  configPath,
22831
23971
  document,
22832
23972
  config,
@@ -22858,7 +23998,7 @@ async function findSkillFiles(root) {
22858
23998
  async function collectSkillFiles(directory, results) {
22859
23999
  const entries = await readdir11(directory, { withFileTypes: true }).catch(
22860
24000
  (error) => {
22861
- if (isNodeError18(error, "ENOENT")) {
24001
+ if (isNodeError19(error, "ENOENT")) {
22862
24002
  return [];
22863
24003
  }
22864
24004
  throw error;
@@ -22870,7 +24010,7 @@ async function collectSkillFiles(directory, results) {
22870
24010
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
22871
24011
  continue;
22872
24012
  }
22873
- const entryPath = path23.join(directory, entry.name);
24013
+ const entryPath = path24.join(directory, entry.name);
22874
24014
  if (entry.isDirectory()) {
22875
24015
  await collectSkillFiles(entryPath, results);
22876
24016
  continue;
@@ -22881,9 +24021,9 @@ async function collectSkillFiles(directory, results) {
22881
24021
  }
22882
24022
  }
22883
24023
  async function readSkillMetadata(input) {
22884
- const raw = await readFile16(input.skillFile, "utf8").catch(
24024
+ const raw = await readFile17(input.skillFile, "utf8").catch(
22885
24025
  (error) => {
22886
- if (isNodeError18(error, "ENOENT") || isNodeError18(error, "EACCES")) {
24026
+ if (isNodeError19(error, "ENOENT") || isNodeError19(error, "EACCES")) {
22887
24027
  return null;
22888
24028
  }
22889
24029
  throw error;
@@ -22892,10 +24032,10 @@ async function readSkillMetadata(input) {
22892
24032
  if (raw === null) {
22893
24033
  return null;
22894
24034
  }
22895
- const skillDir = path23.dirname(input.skillFile);
24035
+ const skillDir = path24.dirname(input.skillFile);
22896
24036
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
22897
24037
  const name = normalizeSkillName(
22898
- readString18(frontmatter.name) ?? path23.basename(skillDir)
24038
+ readString18(frontmatter.name) ?? path24.basename(skillDir)
22899
24039
  );
22900
24040
  if (!name) {
22901
24041
  return null;
@@ -22914,7 +24054,7 @@ async function readSkillMetadata(input) {
22914
24054
  enabled: !input.disabled.has(name),
22915
24055
  source: provenance.source,
22916
24056
  trust: provenance.trust,
22917
- relativePath: path23.relative(input.skillsRoot, skillDir)
24057
+ relativePath: path24.relative(input.skillsRoot, skillDir)
22918
24058
  };
22919
24059
  }
22920
24060
  function parseSkillDocument(raw) {
@@ -22927,7 +24067,7 @@ function parseSkillDocument(raw) {
22927
24067
  }
22928
24068
  try {
22929
24069
  return {
22930
- frontmatter: toRecord17(YAML5.parse(match[1] ?? "")),
24070
+ frontmatter: toRecord18(YAML6.parse(match[1] ?? "")),
22931
24071
  body: content.slice(match[0].length)
22932
24072
  };
22933
24073
  } catch {
@@ -22935,8 +24075,8 @@ function parseSkillDocument(raw) {
22935
24075
  }
22936
24076
  }
22937
24077
  function categoryFromPath(skillsRoot, skillFile) {
22938
- const relative = path23.relative(skillsRoot, skillFile);
22939
- const parts = relative.split(path23.sep).filter(Boolean);
24078
+ const relative = path24.relative(skillsRoot, skillFile);
24079
+ const parts = relative.split(path24.sep).filter(Boolean);
22940
24080
  return parts.length >= 3 ? parts[0] : null;
22941
24081
  }
22942
24082
  function firstBodyDescription(body) {
@@ -22959,8 +24099,8 @@ function normalizeDescription(value) {
22959
24099
  return `${description.slice(0, MAX_DESCRIPTION_LENGTH - 3)}...`;
22960
24100
  }
22961
24101
  async function readDisabledSkillNames(configPath) {
22962
- const raw = await readFile16(configPath, "utf8").catch((error) => {
22963
- if (isNodeError18(error, "ENOENT")) {
24102
+ const raw = await readFile17(configPath, "utf8").catch((error) => {
24103
+ if (isNodeError19(error, "ENOENT")) {
22964
24104
  return "";
22965
24105
  }
22966
24106
  throw error;
@@ -22968,8 +24108,8 @@ async function readDisabledSkillNames(configPath) {
22968
24108
  if (!raw.trim()) {
22969
24109
  return /* @__PURE__ */ new Set();
22970
24110
  }
22971
- const config = toRecord17(YAML5.parse(raw));
22972
- const skills = toRecord17(config.skills);
24111
+ const config = toRecord18(YAML6.parse(raw));
24112
+ const skills = toRecord18(config.skills);
22973
24113
  return new Set(readStringList3(skills.disabled));
22974
24114
  }
22975
24115
  async function readSkillProvenance(root) {
@@ -22983,9 +24123,9 @@ async function readSkillProvenance(root) {
22983
24123
  return provenance;
22984
24124
  }
22985
24125
  async function readBundledSkillNames(root) {
22986
- const raw = await readFile16(path23.join(root, ".bundled_manifest"), "utf8").catch(
24126
+ const raw = await readFile17(path24.join(root, ".bundled_manifest"), "utf8").catch(
22987
24127
  (error) => {
22988
- if (isNodeError18(error, "ENOENT")) {
24128
+ if (isNodeError19(error, "ENOENT")) {
22989
24129
  return "";
22990
24130
  }
22991
24131
  throw error;
@@ -23006,9 +24146,9 @@ async function readBundledSkillNames(root) {
23006
24146
  return names;
23007
24147
  }
23008
24148
  async function readHubInstalledSkills(root) {
23009
- 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(
23010
24150
  (error) => {
23011
- if (isNodeError18(error, "ENOENT")) {
24151
+ if (isNodeError19(error, "ENOENT")) {
23012
24152
  return "";
23013
24153
  }
23014
24154
  throw error;
@@ -23019,14 +24159,14 @@ async function readHubInstalledSkills(root) {
23019
24159
  }
23020
24160
  let lock;
23021
24161
  try {
23022
- lock = toRecord17(JSON.parse(raw));
24162
+ lock = toRecord18(JSON.parse(raw));
23023
24163
  } catch {
23024
24164
  return /* @__PURE__ */ new Map();
23025
24165
  }
23026
- const installed = toRecord17(lock.installed);
24166
+ const installed2 = toRecord18(lock.installed);
23027
24167
  const result = /* @__PURE__ */ new Map();
23028
- for (const [name, rawEntry] of Object.entries(installed)) {
23029
- const entry = toRecord17(rawEntry);
24168
+ for (const [name, rawEntry] of Object.entries(installed2)) {
24169
+ const entry = toRecord18(rawEntry);
23030
24170
  result.set(normalizeSkillName(name), {
23031
24171
  source: readString18(entry.source) ?? "hub",
23032
24172
  trust: readString18(entry.trust_level) ?? null
@@ -23077,23 +24217,23 @@ function compareCategoryNames(left, right) {
23077
24217
  }
23078
24218
  return left.localeCompare(right);
23079
24219
  }
23080
- async function readHermesConfigDocument2(configPath) {
23081
- const existingRaw = await readFile16(configPath, "utf8").catch(
24220
+ async function readHermesConfigDocument3(configPath) {
24221
+ const existingRaw = await readFile17(configPath, "utf8").catch(
23082
24222
  (error) => {
23083
- if (isNodeError18(error, "ENOENT")) {
24223
+ if (isNodeError19(error, "ENOENT")) {
23084
24224
  return null;
23085
24225
  }
23086
24226
  throw error;
23087
24227
  }
23088
24228
  );
23089
- const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
24229
+ const document = existingRaw ? YAML6.parseDocument(existingRaw) : new YAML6.Document({});
23090
24230
  return {
23091
24231
  document,
23092
- config: toRecord17(document.toJSON()),
24232
+ config: toRecord18(document.toJSON()),
23093
24233
  existingRaw
23094
24234
  };
23095
24235
  }
23096
- async function writeHermesConfigDocument2(input) {
24236
+ async function writeHermesConfigDocument3(input) {
23097
24237
  const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
23098
24238
  if (backupPath) {
23099
24239
  await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
@@ -23116,18 +24256,18 @@ function readStringList3(value) {
23116
24256
  function readString18(value) {
23117
24257
  return typeof value === "string" && value.trim() ? value.trim() : null;
23118
24258
  }
23119
- function toRecord17(value) {
24259
+ function toRecord18(value) {
23120
24260
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
23121
24261
  }
23122
- function ensureRecord3(target, key) {
23123
- const current = toRecord17(target[key]);
24262
+ function ensureRecord4(target, key) {
24263
+ const current = toRecord18(target[key]);
23124
24264
  if (current === target[key]) {
23125
24265
  return current;
23126
24266
  }
23127
24267
  target[key] = current;
23128
24268
  return current;
23129
24269
  }
23130
- function isNodeError18(error, code) {
24270
+ function isNodeError19(error, code) {
23131
24271
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
23132
24272
  }
23133
24273
 
@@ -23610,8 +24750,8 @@ function readModelList(payload) {
23610
24750
  // src/hermes/updates.ts
23611
24751
  import { EventEmitter as EventEmitter3 } from "events";
23612
24752
  import { spawn as spawn3 } from "child_process";
23613
- import { mkdir as mkdir12, readFile as readFile17, rm as rm7 } from "fs/promises";
23614
- import path24 from "path";
24753
+ import { mkdir as mkdir12, readFile as readFile18, rm as rm7 } from "fs/promises";
24754
+ import path25 from "path";
23615
24755
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
23616
24756
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
23617
24757
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -23844,7 +24984,7 @@ async function readRemoteRelease(options, now) {
23844
24984
  }
23845
24985
  }
23846
24986
  function normalizeServerReleaseSnapshot(payload) {
23847
- const snapshot = toRecord18(payload);
24987
+ const snapshot = toRecord19(payload);
23848
24988
  const remote = toNullableRecord(snapshot.remote);
23849
24989
  return {
23850
24990
  remote: remote ? normalizeServerRelease(remote) : null,
@@ -23880,7 +25020,7 @@ async function writeUpdateState(paths, state) {
23880
25020
  await writeJsonFile(updateStatePath(paths), state);
23881
25021
  }
23882
25022
  async function readUpdateLogLines(paths) {
23883
- const raw = await readFile17(updateLogPath(paths), "utf8").catch(() => "");
25023
+ const raw = await readFile18(updateLogPath(paths), "utf8").catch(() => "");
23884
25024
  if (!raw.trim()) {
23885
25025
  return [];
23886
25026
  }
@@ -23889,13 +25029,13 @@ async function readUpdateLogLines(paths) {
23889
25029
  );
23890
25030
  }
23891
25031
  function releaseCachePath(paths) {
23892
- return path24.join(paths.indexesDir, "hermes-release-check.json");
25032
+ return path25.join(paths.indexesDir, "hermes-release-check.json");
23893
25033
  }
23894
25034
  function updateStatePath(paths) {
23895
- return path24.join(paths.runDir, "hermes-update-state.json");
25035
+ return path25.join(paths.runDir, "hermes-update-state.json");
23896
25036
  }
23897
25037
  function updateLogPath(paths) {
23898
- return path24.join(paths.logsDir, UPDATE_LOG_FILE);
25038
+ return path25.join(paths.logsDir, UPDATE_LOG_FILE);
23899
25039
  }
23900
25040
  async function clearUpdateLogFiles(paths) {
23901
25041
  const primary = updateLogPath(paths);
@@ -23933,7 +25073,7 @@ function compareSemver2(left, right) {
23933
25073
  }
23934
25074
  return 0;
23935
25075
  }
23936
- function toRecord18(value) {
25076
+ function toRecord19(value) {
23937
25077
  return typeof value === "object" && value !== null ? value : {};
23938
25078
  }
23939
25079
  function toNullableRecord(value) {
@@ -23995,13 +25135,13 @@ function readString19(payload, key) {
23995
25135
  // src/link/updates.ts
23996
25136
  import { spawn as spawn5 } from "child_process";
23997
25137
  import { EventEmitter as EventEmitter4 } from "events";
23998
- import { mkdir as mkdir15, readFile as readFile19, rm as rm10 } from "fs/promises";
23999
- import path26 from "path";
25138
+ import { mkdir as mkdir15, readFile as readFile20, rm as rm10 } from "fs/promises";
25139
+ import path27 from "path";
24000
25140
 
24001
25141
  // src/daemon/process.ts
24002
25142
  import { spawn as spawn4 } from "child_process";
24003
- import { mkdir as mkdir14, readFile as readFile18, rm as rm9 } from "fs/promises";
24004
- 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";
24005
25145
 
24006
25146
  // src/daemon/service.ts
24007
25147
  import { createServer } from "http";
@@ -24010,31 +25150,146 @@ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promise
24010
25150
  // src/relay/control-client.ts
24011
25151
  import WebSocket from "ws";
24012
25152
 
24013
- // src/relay/stream-policy.ts
24014
- var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
24015
- flushIntervalMs: 1e3,
24016
- flushBytes: 4 * 1024
24017
- };
24018
- var RELAY_STREAM_POLICY_CONSTRAINTS = {
24019
- flushIntervalMs: {
24020
- min: 50,
24021
- max: 1e3
24022
- },
24023
- flushBytes: {
24024
- min: 1024,
24025
- max: 64 * 1024
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;
24026
25165
  }
24027
- };
24028
- async function fetchRelayStreamBatchPolicy(serverBaseUrl, options = {}) {
24029
- const fetchImpl = options.fetchImpl ?? fetch;
24030
- const controller = new AbortController();
24031
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
24032
- timeout.unref?.();
24033
- try {
24034
- const response = await fetchImpl(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/stream-policy`, {
24035
- headers: {
24036
- accept: "application/json"
24037
- },
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
+
25268
+ // src/relay/stream-policy.ts
25269
+ var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
25270
+ flushIntervalMs: 1e3,
25271
+ flushBytes: 4 * 1024
25272
+ };
25273
+ var RELAY_STREAM_POLICY_CONSTRAINTS = {
25274
+ flushIntervalMs: {
25275
+ min: 50,
25276
+ max: 1e3
25277
+ },
25278
+ flushBytes: {
25279
+ min: 1024,
25280
+ max: 64 * 1024
25281
+ }
25282
+ };
25283
+ async function fetchRelayStreamBatchPolicy(serverBaseUrl, options = {}) {
25284
+ const fetchImpl = options.fetchImpl ?? fetch;
25285
+ const controller = new AbortController();
25286
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
25287
+ timeout.unref?.();
25288
+ try {
25289
+ const response = await fetchImpl(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/stream-policy`, {
25290
+ headers: {
25291
+ accept: "application/json"
25292
+ },
24038
25293
  signal: controller.signal
24039
25294
  });
24040
25295
  if (!response.ok) {
@@ -24080,23 +25335,76 @@ function connectRelayControl(options) {
24080
25335
  const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
24081
25336
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
24082
25337
  wsUrl.searchParams.set("link_id", options.linkId);
24083
- const maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
24084
- const backoffBaseMs = options.backoffBaseMs ?? 1e3;
24085
- 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;
24086
25342
  let reconnectAttempts = 0;
24087
25343
  let closedByUser = false;
24088
25344
  let socket = null;
24089
25345
  let retryTimer = null;
24090
25346
  let abortControllers = /* @__PURE__ */ new Map();
24091
25347
  let fatalRelayRejection = null;
25348
+ let relayRetryAfterMs = null;
24092
25349
  let latestNetworkRoutes = null;
24093
25350
  const streamBatchPolicy = {
24094
25351
  current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
24095
25352
  onUpdate: options.onStreamBatchPolicy
24096
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
+ };
24097
25372
  const connect = () => {
24098
25373
  options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
24099
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
+ };
24100
25408
  socket = new WebSocket(wsUrl, {
24101
25409
  headers: {
24102
25410
  "x-hermes-link-version": LINK_VERSION
@@ -24104,6 +25412,7 @@ function connectRelayControl(options) {
24104
25412
  });
24105
25413
  socket.on("open", () => {
24106
25414
  reconnectAttempts = 0;
25415
+ void clearRelayReconnectState(paths).catch(() => void 0);
24107
25416
  options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
24108
25417
  const currentSocket = socket;
24109
25418
  if (currentSocket && latestNetworkRoutes) {
@@ -24119,6 +25428,20 @@ function connectRelayControl(options) {
24119
25428
  socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
24120
25429
  });
24121
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
+ });
24122
25445
  socket.on("error", (error) => {
24123
25446
  const message = error instanceof Error ? error.message : "Relay websocket error";
24124
25447
  fatalRelayRejection = resolveFatalRelayRejection(message);
@@ -24129,32 +25452,40 @@ function connectRelayControl(options) {
24129
25452
  });
24130
25453
  });
24131
25454
  socket.on("close", () => {
24132
- abortAll(abortControllers);
24133
- abortControllers = /* @__PURE__ */ new Map();
24134
- if (fatalRelayRejection) {
24135
- options.onStatus?.({
24136
- state: "failed",
24137
- attempt: reconnectAttempts,
24138
- message: fatalRelayRejection
24139
- });
24140
- return;
24141
- }
24142
- if (closedByUser) {
24143
- options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
24144
- return;
24145
- }
24146
- if (reconnectAttempts >= maxReconnectAttempts) {
24147
- options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
24148
- return;
24149
- }
24150
- reconnectAttempts += 1;
24151
- const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
24152
- options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
24153
- retryTimer = setTimeout(connect, delay3);
24154
- retryTimer.unref?.();
25455
+ handleConnectionClosed();
24155
25456
  });
24156
25457
  };
24157
- 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
+ }
24158
25489
  return {
24159
25490
  publishNetworkRoutes(routes) {
24160
25491
  latestNetworkRoutes = routes;
@@ -24191,7 +25522,12 @@ function sendNetworkRoutes(socket, linkId, routes) {
24191
25522
  }));
24192
25523
  }
24193
25524
  function resolveFatalRelayRejection(message) {
24194
- 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)) {
24195
25531
  return null;
24196
25532
  }
24197
25533
  return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
@@ -24202,10 +25538,21 @@ function abortAll(abortControllers) {
24202
25538
  }
24203
25539
  abortControllers.clear();
24204
25540
  }
24205
- function computeBackoffMs(attempt, baseMs, maxMs) {
24206
- const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
24207
- const jitter = Math.floor(Math.random() * Math.min(1e3, exponential * 0.2));
24208
- 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());
24209
25556
  }
24210
25557
  async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
24211
25558
  const frame = JSON.parse(raw);
@@ -24326,10 +25673,58 @@ function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
24326
25673
  };
24327
25674
  }
24328
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
+
24329
25724
  // src/runtime/system-info.ts
24330
25725
  import { execFileSync } from "child_process";
24331
25726
  import { readFileSync } from "fs";
24332
- import os4 from "os";
25727
+ import os5 from "os";
24333
25728
  function readLinkSystemInfo() {
24334
25729
  const platform = process.platform;
24335
25730
  const hostname = readHostname(platform);
@@ -24368,7 +25763,7 @@ function readHostname(platform) {
24368
25763
  return computerName;
24369
25764
  }
24370
25765
  }
24371
- return normalizeText(os4.hostname());
25766
+ return normalizeText(os5.hostname());
24372
25767
  }
24373
25768
  function readOsLabel(platform) {
24374
25769
  if (platform === "darwin") {
@@ -24376,12 +25771,12 @@ function readOsLabel(platform) {
24376
25771
  return version ? `macOS ${version}` : "macOS";
24377
25772
  }
24378
25773
  if (platform === "linux") {
24379
- return readLinuxOsRelease() ?? `Linux ${os4.release()}`;
25774
+ return readLinuxOsRelease() ?? `Linux ${os5.release()}`;
24380
25775
  }
24381
25776
  if (platform === "win32") {
24382
- return `Windows ${os4.release()}`;
25777
+ return `Windows ${os5.release()}`;
24383
25778
  }
24384
- return `${os4.type()} ${os4.release()}`.trim();
25779
+ return `${os5.type()} ${os5.release()}`.trim();
24385
25780
  }
24386
25781
  function readLinuxOsRelease() {
24387
25782
  for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
@@ -24428,11 +25823,11 @@ function truncateText(value, maxLength) {
24428
25823
  }
24429
25824
 
24430
25825
  // src/topology/network.ts
24431
- import os6 from "os";
25826
+ import os7 from "os";
24432
25827
 
24433
25828
  // src/topology/environment.ts
24434
25829
  import { existsSync, readFileSync as readFileSync2 } from "fs";
24435
- import os5 from "os";
25830
+ import os6 from "os";
24436
25831
  function detectRuntimeEnvironment(env = process.env) {
24437
25832
  if (isWsl(env)) {
24438
25833
  return {
@@ -24461,7 +25856,7 @@ function isWsl(env) {
24461
25856
  if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
24462
25857
  return true;
24463
25858
  }
24464
- const release = os5.release().toLowerCase();
25859
+ const release = os6.release().toLowerCase();
24465
25860
  return release.includes("microsoft") || release.includes("wsl");
24466
25861
  }
24467
25862
  function isContainer(env) {
@@ -24506,7 +25901,7 @@ async function discoverRouteCandidates(options) {
24506
25901
  };
24507
25902
  }
24508
25903
  function discoverLanIps() {
24509
- return discoverLanIpsFromInterfaces(os6.networkInterfaces());
25904
+ return discoverLanIpsFromInterfaces(os7.networkInterfaces());
24510
25905
  }
24511
25906
  function discoverLanIpsFromInterfaces(interfaces) {
24512
25907
  const result = /* @__PURE__ */ new Set();
@@ -24645,7 +26040,7 @@ function unique(values) {
24645
26040
  // src/link/network-report-state.ts
24646
26041
  var DEFAULT_AUTO_DAILY_LIMIT = 20;
24647
26042
  async function readNetworkReportState(paths) {
24648
- const state = await readLinkState(paths);
26043
+ const state = await readLinkState3(paths);
24649
26044
  return normalizeNetworkReportState(state.networkReport);
24650
26045
  }
24651
26046
  async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
@@ -24712,14 +26107,14 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
24712
26107
  };
24713
26108
  }
24714
26109
  async function updateNetworkReportState(paths, update) {
24715
- const state = await readLinkState(paths);
26110
+ const state = await readLinkState3(paths);
24716
26111
  const next = {
24717
26112
  ...state,
24718
26113
  networkReport: update(normalizeNetworkReportState(state.networkReport))
24719
26114
  };
24720
26115
  await writeJsonFile(paths.stateFile, next);
24721
26116
  }
24722
- async function readLinkState(paths) {
26117
+ async function readLinkState3(paths) {
24723
26118
  const state = await readJsonFile(paths.stateFile);
24724
26119
  return state && typeof state === "object" ? state : {};
24725
26120
  }
@@ -24992,6 +26387,89 @@ async function checkLanIpChange(options, context = {}) {
24992
26387
  }
24993
26388
  }
24994
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
+
24995
26473
  // src/daemon/scheduler.ts
24996
26474
  function startCronDeliveryScheduler(options) {
24997
26475
  let running = false;
@@ -25077,6 +26555,11 @@ async function startLinkService(options = {}) {
25077
26555
  current_version: migration.currentVersion
25078
26556
  });
25079
26557
  }
26558
+ await ensureHermesLinkSkillInstalledBestEffort({
26559
+ paths,
26560
+ logger,
26561
+ source: "service_startup"
26562
+ });
25080
26563
  const conversations = new ConversationService(paths, logger);
25081
26564
  await conversations.rebuildStatisticsIndex();
25082
26565
  let relay = null;
@@ -25108,6 +26591,11 @@ async function startLinkService(options = {}) {
25108
26591
  logger,
25109
26592
  conversations,
25110
26593
  onPairingClaimed: async () => {
26594
+ void ensureHermesLinkSkillInstalledBestEffort({
26595
+ paths,
26596
+ logger,
26597
+ source: "pairing_claimed"
26598
+ });
25111
26599
  triggerHermesSessionSync();
25112
26600
  void loadRelayStreamBatchPolicy("pairing_claimed");
25113
26601
  await options.onPairingClaimed?.();
@@ -25132,6 +26620,38 @@ async function startLinkService(options = {}) {
25132
26620
  error: error.message
25133
26621
  });
25134
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
+ });
25135
26655
  void logger.info("service_started", {
25136
26656
  port: config.port,
25137
26657
  link_id: identity?.link_id ?? null
@@ -25157,9 +26677,8 @@ async function startLinkService(options = {}) {
25157
26677
  relayBaseUrl: config.relayBaseUrl,
25158
26678
  linkId: identity.link_id,
25159
26679
  localPort: config.port,
25160
- maxReconnectAttempts: options.relayMaxReconnectAttempts ?? 5,
25161
- backoffBaseMs: 1e3,
25162
- backoffMaxMs: 3e4,
26680
+ paths,
26681
+ maxReconnectAttempts: options.relayMaxReconnectAttempts,
25163
26682
  onStreamBatchPolicy: (policy) => {
25164
26683
  void logger.info("relay_stream_policy_updated", {
25165
26684
  flushIntervalMs: policy.flushIntervalMs,
@@ -25167,6 +26686,7 @@ async function startLinkService(options = {}) {
25167
26686
  });
25168
26687
  },
25169
26688
  onStatus: (status) => {
26689
+ void writeRelayStatusSnapshot(paths, status).catch(() => void 0);
25170
26690
  void logger.info("relay_status", status);
25171
26691
  if (status.state === "connected") {
25172
26692
  const now = Date.now();
@@ -25207,8 +26727,13 @@ async function startLinkService(options = {}) {
25207
26727
  if (options.writePidFile) {
25208
26728
  await writePidFile(paths);
25209
26729
  }
25210
- return {
26730
+ let closed = false;
26731
+ const service = {
25211
26732
  async close() {
26733
+ if (closed) {
26734
+ return;
26735
+ }
26736
+ closed = true;
25212
26737
  relay?.close();
25213
26738
  await closeServer(server);
25214
26739
  await Promise.all([
@@ -25224,6 +26749,12 @@ async function startLinkService(options = {}) {
25224
26749
  }
25225
26750
  }
25226
26751
  };
26752
+ if (options.writePidFile) {
26753
+ installDaemonProcessGuard(logger, {
26754
+ onFatal: () => service.close()
26755
+ });
26756
+ }
26757
+ return service;
25227
26758
  }
25228
26759
  function waitForRelayReadyTimeout(timeoutMs) {
25229
26760
  return new Promise((resolve) => {
@@ -25278,6 +26809,16 @@ async function closeServer(server) {
25278
26809
  server.closeIdleConnections?.();
25279
26810
  });
25280
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
+ }
25281
26822
  async function listenServer(server, port) {
25282
26823
  await new Promise((resolve, reject) => {
25283
26824
  const cleanup = () => {
@@ -25299,6 +26840,16 @@ async function listenServer(server, port) {
25299
26840
  }
25300
26841
 
25301
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";
25302
26853
  async function startDaemonProcess(paths = resolveRuntimePaths()) {
25303
26854
  const config = await loadConfig(paths);
25304
26855
  let status = await getDaemonStatus(paths);
@@ -25323,7 +26874,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
25323
26874
  });
25324
26875
  child.unref();
25325
26876
  for (let index = 0; index < 12; index += 1) {
25326
- await wait(250);
26877
+ await wait2(250);
25327
26878
  const next = await getDaemonStatus(paths);
25328
26879
  if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
25329
26880
  return next;
@@ -25335,43 +26886,92 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
25335
26886
  await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
25336
26887
  const log = createRotatingTextLogWriter({
25337
26888
  paths,
25338
- fileName: path25.basename(daemonLogFile(paths))
26889
+ fileName: path26.basename(daemonLogFile(paths))
25339
26890
  });
25340
26891
  const scriptPath = currentCliScriptPath();
25341
- const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
25342
- stdio: ["ignore", "pipe", "pipe"],
25343
- env: process.env
25344
- });
25345
26892
  const write = (chunk) => {
25346
26893
  void log.write(chunk);
25347
26894
  };
25348
26895
  write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
25349
26896
  `);
25350
- child.stdout?.on("data", write);
25351
- 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;
25352
26906
  const forwardStop = () => {
25353
- if (child.pid && isProcessAlive3(child.pid)) {
26907
+ stopRequested = true;
26908
+ if (child?.pid && isProcessAlive3(child.pid)) {
25354
26909
  child.kill("SIGTERM");
25355
26910
  }
25356
26911
  };
25357
26912
  process.once("SIGINT", forwardStop);
25358
26913
  process.once("SIGTERM", forwardStop);
25359
- const result = await new Promise((resolve, reject) => {
25360
- child.once("error", reject);
25361
- child.once("exit", (code, signal) => resolve({ code, signal }));
25362
- }).catch((error) => {
25363
- write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor failed: ${error instanceof Error ? error.message : String(error)}
25364
- `);
25365
- return { code: 1, signal: null };
25366
- });
25367
- process.off("SIGINT", forwardStop);
25368
- 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
+ }
25369
26969
  write(
25370
- `[${(/* @__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"}
25371
26971
  `
25372
26972
  );
25373
26973
  await log.flush();
25374
- return result.code ?? (result.signal ? 0 : 1);
26974
+ return finalResult.code ?? (finalResult.signal ? 0 : 1);
25375
26975
  }
25376
26976
  async function probeLocalLinkService(options) {
25377
26977
  const unreachable = {
@@ -25409,6 +27009,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25409
27009
  if (!status.running || !status.pid) {
25410
27010
  return status;
25411
27011
  }
27012
+ await writeSupervisorStopIntent(paths, status.pid).catch(() => void 0);
25412
27013
  try {
25413
27014
  process.kill(status.pid, "SIGTERM");
25414
27015
  } catch {
@@ -25416,7 +27017,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25416
27017
  return await getDaemonStatus(paths);
25417
27018
  }
25418
27019
  for (let index = 0; index < 20; index += 1) {
25419
- await wait(250);
27020
+ await wait2(250);
25420
27021
  if (!isProcessAlive3(status.pid)) {
25421
27022
  break;
25422
27023
  }
@@ -25427,7 +27028,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
25427
27028
  } catch {
25428
27029
  }
25429
27030
  for (let index = 0; index < 10; index += 1) {
25430
- await wait(250);
27031
+ await wait2(250);
25431
27032
  if (!isProcessAlive3(status.pid)) {
25432
27033
  break;
25433
27034
  }
@@ -25464,7 +27065,7 @@ function currentCliScriptPath() {
25464
27065
  return process.argv[1];
25465
27066
  }
25466
27067
  async function readPid(filePath) {
25467
- const raw = await readFile18(filePath, "utf8").catch(() => null);
27068
+ const raw = await readFile19(filePath, "utf8").catch(() => null);
25468
27069
  if (!raw) {
25469
27070
  return null;
25470
27071
  }
@@ -25479,6 +27080,171 @@ function isProcessAlive3(pid) {
25479
27080
  return false;
25480
27081
  }
25481
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
+ }
25482
27248
  async function pidBackedServiceIsReachable(paths) {
25483
27249
  const config = await loadConfig(paths).catch(() => null);
25484
27250
  if (!config) {
@@ -25486,7 +27252,7 @@ async function pidBackedServiceIsReachable(paths) {
25486
27252
  }
25487
27253
  return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
25488
27254
  }
25489
- function wait(ms) {
27255
+ function wait2(ms) {
25490
27256
  return new Promise((resolve) => setTimeout(resolve, ms));
25491
27257
  }
25492
27258
 
@@ -25893,7 +27659,7 @@ async function readRemoteLinkPolicy(options) {
25893
27659
  }
25894
27660
  }
25895
27661
  function normalizeServerSnapshot(payload) {
25896
- const snapshot = toRecord19(payload);
27662
+ const snapshot = toRecord20(payload);
25897
27663
  const policy = toNullableRecord2(snapshot.policy);
25898
27664
  if (!policy) {
25899
27665
  return {
@@ -26236,7 +28002,7 @@ async function writeUpdateState2(paths, state) {
26236
28002
  await writeJsonFile(updateStatePath2(paths), state);
26237
28003
  }
26238
28004
  async function readUpdateLogLines2(paths) {
26239
- const raw = await readFile19(updateLogPath2(paths), "utf8").catch(() => "");
28005
+ const raw = await readFile20(updateLogPath2(paths), "utf8").catch(() => "");
26240
28006
  if (!raw.trim()) {
26241
28007
  return [];
26242
28008
  }
@@ -26245,10 +28011,10 @@ async function readUpdateLogLines2(paths) {
26245
28011
  );
26246
28012
  }
26247
28013
  function updateStatePath2(paths) {
26248
- return path26.join(paths.runDir, "link-update-state.json");
28014
+ return path27.join(paths.runDir, "link-update-state.json");
26249
28015
  }
26250
28016
  function updateLogPath2(paths) {
26251
- return path26.join(paths.logsDir, UPDATE_LOG_FILE2);
28017
+ return path27.join(paths.logsDir, UPDATE_LOG_FILE2);
26252
28018
  }
26253
28019
  async function clearUpdateLogFiles2(paths) {
26254
28020
  const primary = updateLogPath2(paths);
@@ -26320,7 +28086,7 @@ function isProcessAlive4(pid) {
26320
28086
  return false;
26321
28087
  }
26322
28088
  }
26323
- function toRecord19(value) {
28089
+ function toRecord20(value) {
26324
28090
  return typeof value === "object" && value !== null ? value : {};
26325
28091
  }
26326
28092
  function toNullableRecord2(value) {
@@ -26332,7 +28098,7 @@ function readString20(payload, key) {
26332
28098
  }
26333
28099
 
26334
28100
  // src/pairing/pairing.ts
26335
- import path27 from "path";
28101
+ import path28 from "path";
26336
28102
  import { rm as rm11 } from "fs/promises";
26337
28103
 
26338
28104
  // src/relay/bootstrap.ts
@@ -26672,10 +28438,10 @@ async function loadRequiredIdentity2(paths) {
26672
28438
  }
26673
28439
  return identity;
26674
28440
  }
26675
- async function postServerJson(serverBaseUrl, path28, body, options) {
28441
+ async function postServerJson(serverBaseUrl, path29, body, options) {
26676
28442
  let response;
26677
28443
  try {
26678
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path28}`, {
28444
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
26679
28445
  method: "POST",
26680
28446
  headers: {
26681
28447
  accept: "application/json",
@@ -26723,10 +28489,10 @@ function pairingErrorSnapshot(stage, error) {
26723
28489
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
26724
28490
  };
26725
28491
  }
26726
- async function patchServerJson(serverBaseUrl, path28, token, body, options) {
28492
+ async function patchServerJson(serverBaseUrl, path29, token, body, options) {
26727
28493
  let response;
26728
28494
  try {
26729
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path28}`, {
28495
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
26730
28496
  method: "PATCH",
26731
28497
  headers: {
26732
28498
  accept: "application/json",
@@ -26774,10 +28540,10 @@ function createPairingNetworkError(input) {
26774
28540
  );
26775
28541
  }
26776
28542
  function pairingClaimPath(sessionId, paths) {
26777
- 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`);
26778
28544
  }
26779
28545
  function pairingSessionPath(sessionId, paths) {
26780
- return path27.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
28546
+ return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
26781
28547
  }
26782
28548
  function qrPreferredUrls(routes) {
26783
28549
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -26891,7 +28657,12 @@ function registerSystemRoutes(router, options) {
26891
28657
  error: error instanceof Error ? error.message : String(error)
26892
28658
  });
26893
28659
  });
26894
- 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
+ });
26895
28666
  }, 250);
26896
28667
  timer.unref?.();
26897
28668
  });
@@ -27929,6 +29700,18 @@ async function createApp(options = {}) {
27929
29700
  };
27930
29701
  const app = new Koa();
27931
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
+ });
27932
29715
  app.use(createHttpErrorMiddleware(logger));
27933
29716
  registerSystemRoutes(router, {
27934
29717
  paths,
@@ -27967,6 +29750,11 @@ export {
27967
29750
  resolveRuntimePaths,
27968
29751
  createFileLogger,
27969
29752
  getLinkLogFile,
29753
+ readRecentLogEntries,
29754
+ readRecentTextLogEntries,
29755
+ getGatewayLogFiles,
29756
+ readRecentGatewayLogEntries,
29757
+ flushLogFiles,
27970
29758
  ensureHermesApiServerAvailable,
27971
29759
  readHermesVersion,
27972
29760
  defaultLinkConfig,
@@ -27979,6 +29767,7 @@ export {
27979
29767
  getIdentityStatus,
27980
29768
  ConversationService,
27981
29769
  hasActiveDevices,
29770
+ ensureHermesLinkSkillInstalledBestEffort,
27982
29771
  detectRuntimeEnvironment,
27983
29772
  preparePairing,
27984
29773
  readPairingClaim,
@@ -27986,6 +29775,7 @@ export {
27986
29775
  createApp,
27987
29776
  fetchRelayStreamBatchPolicy,
27988
29777
  connectRelayControl,
29778
+ readRelayStatusSnapshot,
27989
29779
  reportLinkStatusToServer,
27990
29780
  startLinkService,
27991
29781
  startDaemonProcess,