@hermespilot/link 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/{chunk-UHYO4EJD.js → chunk-UBWRPRZ4.js} +2173 -489
- package/dist/cli/index.js +615 -25
- package/dist/http/app.js +1 -1
- package/package.json +20 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
1993
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2569
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
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:
|
|
2593
|
-
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
|
-
|
|
2599
|
-
|
|
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:
|
|
4791
|
+
return files.sort((left, right) => left.mtimeMs - right.mtimeMs).map(({ path: path29, mtime }) => ({ path: path29, mtime }));
|
|
4429
4792
|
}
|
|
4430
4793
|
async function readCronOutput(outputPath) {
|
|
4431
4794
|
const content = await readFile3(outputPath, "utf8");
|
|
@@ -4493,7 +4856,7 @@ import { setTimeout as delay } from "timers/promises";
|
|
|
4493
4856
|
import { promisify as promisify2 } from "util";
|
|
4494
4857
|
|
|
4495
4858
|
// src/runtime/logger.ts
|
|
4496
|
-
import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3 } from "fs/promises";
|
|
4859
|
+
import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, truncate } from "fs/promises";
|
|
4497
4860
|
import os3 from "os";
|
|
4498
4861
|
import path6 from "path";
|
|
4499
4862
|
|
|
@@ -4502,7 +4865,7 @@ import os2 from "os";
|
|
|
4502
4865
|
import path5 from "path";
|
|
4503
4866
|
|
|
4504
4867
|
// src/constants.ts
|
|
4505
|
-
var LINK_VERSION = "0.6.
|
|
4868
|
+
var LINK_VERSION = "0.6.2";
|
|
4506
4869
|
var LINK_COMMAND = "hermeslink";
|
|
4507
4870
|
var LINK_DEFAULT_PORT = 52379;
|
|
4508
4871
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4719,6 +5082,27 @@ function readRecentGatewayLogEntries(options = {}) {
|
|
|
4719
5082
|
filePaths: options.filePaths ?? getGatewayLogFiles(paths)
|
|
4720
5083
|
});
|
|
4721
5084
|
}
|
|
5085
|
+
async function flushLogFiles(options) {
|
|
5086
|
+
const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
5087
|
+
const truncated = [];
|
|
5088
|
+
const removed = [];
|
|
5089
|
+
const filePaths = Array.from(new Set(options.filePaths.map((filePath) => path6.resolve(filePath))));
|
|
5090
|
+
for (const filePath of filePaths) {
|
|
5091
|
+
if (await fileExists(filePath)) {
|
|
5092
|
+
await truncate(filePath, 0);
|
|
5093
|
+
truncated.push(filePath);
|
|
5094
|
+
}
|
|
5095
|
+
for (let index = 1; index <= maxFiles; index += 1) {
|
|
5096
|
+
const rotated = rotatedLogFile(filePath, index);
|
|
5097
|
+
if (!await fileExists(rotated)) {
|
|
5098
|
+
continue;
|
|
5099
|
+
}
|
|
5100
|
+
await rm2(rotated, { force: true });
|
|
5101
|
+
removed.push(rotated);
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
return { truncated, removed };
|
|
5105
|
+
}
|
|
4722
5106
|
function clampLimit(value) {
|
|
4723
5107
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4724
5108
|
return DEFAULT_READ_LIMIT;
|
|
@@ -4871,6 +5255,10 @@ async function moveIfExists(from, to) {
|
|
|
4871
5255
|
}
|
|
4872
5256
|
});
|
|
4873
5257
|
}
|
|
5258
|
+
async function fileExists(filePath) {
|
|
5259
|
+
const info = await stat3(filePath).catch(() => null);
|
|
5260
|
+
return Boolean(info?.isFile());
|
|
5261
|
+
}
|
|
4874
5262
|
function rotatedLogFile(filePath, index) {
|
|
4875
5263
|
return `${filePath}.${index}`;
|
|
4876
5264
|
}
|
|
@@ -5415,15 +5803,15 @@ ${stderr}`.trim();
|
|
|
5415
5803
|
});
|
|
5416
5804
|
});
|
|
5417
5805
|
}
|
|
5418
|
-
function assertHermesRunsApiSupported(version, status) {
|
|
5806
|
+
function assertHermesRunsApiSupported(version, status, endpoint = "/v1/runs") {
|
|
5419
5807
|
if (status !== 404) {
|
|
5420
5808
|
return;
|
|
5421
5809
|
}
|
|
5422
5810
|
const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
|
|
5423
5811
|
throw new LinkHttpError(
|
|
5424
5812
|
502,
|
|
5425
|
-
"hermes_runs_api_unsupported",
|
|
5426
|
-
`${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
9951
|
-
const subtitle = summarizeAgentArguments({
|
|
10355
|
+
const actionSummary = summarizeAgentArguments({
|
|
9952
10356
|
toolName: name,
|
|
9953
10357
|
rawArguments,
|
|
9954
10358
|
summary,
|
|
9955
10359
|
args
|
|
9956
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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}${
|
|
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:
|
|
13728
|
+
path: path29,
|
|
13219
13729
|
profile: options.profileName ?? "default",
|
|
13220
13730
|
port: config.port ?? null,
|
|
13221
|
-
url: `http://127.0.0.1:${config.port}${
|
|
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,
|
|
13741
|
+
function logHermesApiResponse(logger, method, path29, profileName, startedAt, response) {
|
|
13232
13742
|
const fields = {
|
|
13233
13743
|
method,
|
|
13234
|
-
path:
|
|
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,
|
|
13765
|
+
function logHermesApiError(logger, method, path29, profileName, startedAt, error) {
|
|
13256
13766
|
void logger?.warn("hermes_api_request_failed", {
|
|
13257
13767
|
method,
|
|
13258
|
-
path:
|
|
13768
|
+
path: path29,
|
|
13259
13769
|
profile: profileName ?? "default",
|
|
13260
13770
|
duration_ms: Date.now() - startedAt,
|
|
13261
13771
|
...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
|
|
@@ -14078,24 +14588,29 @@ async function* parseSseResponse(response) {
|
|
|
14078
14588
|
let buffer = "";
|
|
14079
14589
|
for await (const chunk of response.body) {
|
|
14080
14590
|
buffer += decoder.decode(chunk, { stream: true });
|
|
14081
|
-
let
|
|
14082
|
-
while (
|
|
14083
|
-
const block = buffer.slice(0,
|
|
14084
|
-
buffer = buffer.slice(
|
|
14591
|
+
let separator = findSseBlockSeparator(buffer);
|
|
14592
|
+
while (separator) {
|
|
14593
|
+
const block = buffer.slice(0, separator.index);
|
|
14594
|
+
buffer = buffer.slice(separator.index + separator.length);
|
|
14085
14595
|
const parsed = parseSseBlock(block);
|
|
14086
14596
|
if (parsed) {
|
|
14087
14597
|
yield parsed;
|
|
14088
14598
|
}
|
|
14089
|
-
|
|
14599
|
+
separator = findSseBlockSeparator(buffer);
|
|
14090
14600
|
}
|
|
14091
14601
|
}
|
|
14602
|
+
buffer += decoder.decode();
|
|
14092
14603
|
const trailing = parseSseBlock(buffer);
|
|
14093
14604
|
if (trailing) {
|
|
14094
14605
|
yield trailing;
|
|
14095
14606
|
}
|
|
14096
14607
|
}
|
|
14608
|
+
function findSseBlockSeparator(value) {
|
|
14609
|
+
const match = /\r\n\r\n|\n\n|\r\r/u.exec(value);
|
|
14610
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
14611
|
+
}
|
|
14097
14612
|
function parseSseBlock(block) {
|
|
14098
|
-
const lines = block.split(
|
|
14613
|
+
const lines = block.split(/\r\n|\n|\r/u);
|
|
14099
14614
|
let eventName = "";
|
|
14100
14615
|
const data = [];
|
|
14101
14616
|
for (const rawLine of lines) {
|
|
@@ -14155,7 +14670,7 @@ function resolveConversationRunBackend(env = process.env) {
|
|
|
14155
14670
|
if (RUNS_BACKEND_VALUES.has(raw)) {
|
|
14156
14671
|
return "runs";
|
|
14157
14672
|
}
|
|
14158
|
-
return "
|
|
14673
|
+
return "responses";
|
|
14159
14674
|
}
|
|
14160
14675
|
function isRunToolResultCompensationEnabled(env = process.env) {
|
|
14161
14676
|
const raw = env.HERMESLINK_RUN_TOOL_RESULT_COMPENSATION?.trim().toLowerCase();
|
|
@@ -15158,7 +15673,14 @@ var ConversationRunLifecycle = class {
|
|
|
15158
15673
|
runId,
|
|
15159
15674
|
hermesSessionId
|
|
15160
15675
|
);
|
|
15161
|
-
const
|
|
15676
|
+
const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
|
|
15677
|
+
if (previousResponseId) {
|
|
15678
|
+
await this.updateRun(conversationId, runId, {
|
|
15679
|
+
previous_response_id: previousResponseId
|
|
15680
|
+
});
|
|
15681
|
+
}
|
|
15682
|
+
const shouldBuildConversationHistory = backend === "runs" || !previousResponseId;
|
|
15683
|
+
let conversationHistory = shouldBuildConversationHistory ? await buildConversationHistory({
|
|
15162
15684
|
paths: this.deps.paths,
|
|
15163
15685
|
profileName: run.profile,
|
|
15164
15686
|
hermesSessionId,
|
|
@@ -15175,29 +15697,31 @@ var ConversationRunLifecycle = class {
|
|
|
15175
15697
|
source: "empty",
|
|
15176
15698
|
diagnostics: emptyConversationHistoryDiagnostics("build_failed")
|
|
15177
15699
|
};
|
|
15178
|
-
})
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
|
|
15700
|
+
}) : {
|
|
15701
|
+
messages: [],
|
|
15702
|
+
source: "empty",
|
|
15703
|
+
diagnostics: emptyConversationHistoryDiagnostics("no_history")
|
|
15704
|
+
};
|
|
15705
|
+
await this.deps.logger.debug(
|
|
15706
|
+
shouldBuildConversationHistory ? "conversation_history_built" : "conversation_history_skipped_previous_response",
|
|
15707
|
+
{
|
|
15708
|
+
conversation_id: conversationId,
|
|
15709
|
+
run_id: runId,
|
|
15710
|
+
backend,
|
|
15711
|
+
source: conversationHistory.source,
|
|
15712
|
+
message_count: conversationHistory.messages.length,
|
|
15713
|
+
...conversationHistory.diagnostics
|
|
15714
|
+
}
|
|
15715
|
+
);
|
|
15716
|
+
const cronJobIdsBeforeRun = await this.readHermesCronJobIds(
|
|
15717
|
+
run.profile
|
|
15718
|
+
).catch(() => null);
|
|
15719
|
+
const resolvedInput = await this.resolveRunInput({
|
|
15190
15720
|
conversationId,
|
|
15191
15721
|
run,
|
|
15192
15722
|
fallbackInput: input,
|
|
15193
15723
|
snapshot
|
|
15194
15724
|
});
|
|
15195
|
-
const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
|
|
15196
|
-
if (previousResponseId) {
|
|
15197
|
-
await this.updateRun(conversationId, runId, {
|
|
15198
|
-
previous_response_id: previousResponseId
|
|
15199
|
-
});
|
|
15200
|
-
}
|
|
15201
15725
|
const deliveryStagingDir = await prepareDeliveryStagingRunDir(
|
|
15202
15726
|
this.deps.paths,
|
|
15203
15727
|
conversationId,
|
|
@@ -15211,12 +15735,12 @@ var ConversationRunLifecycle = class {
|
|
|
15211
15735
|
return void 0;
|
|
15212
15736
|
});
|
|
15213
15737
|
const instructions = buildRunInstructions(run, deliveryStagingDir);
|
|
15214
|
-
const estimatedUsage = estimateContextUsage({
|
|
15738
|
+
const estimatedUsage = shouldBuildConversationHistory ? estimateContextUsage({
|
|
15215
15739
|
conversationHistory: conversationHistory.messages,
|
|
15216
15740
|
currentInput: resolvedInput,
|
|
15217
15741
|
instructions,
|
|
15218
15742
|
contextWindow: run.context_window
|
|
15219
|
-
});
|
|
15743
|
+
}) : void 0;
|
|
15220
15744
|
if (estimatedUsage) {
|
|
15221
15745
|
await this.updateRun(conversationId, runId, { usage: estimatedUsage });
|
|
15222
15746
|
}
|
|
@@ -15225,22 +15749,89 @@ var ConversationRunLifecycle = class {
|
|
|
15225
15749
|
run.profile ?? "default"
|
|
15226
15750
|
);
|
|
15227
15751
|
if (backend === "responses") {
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15752
|
+
let response;
|
|
15753
|
+
try {
|
|
15754
|
+
response = await streamHermesResponses(
|
|
15755
|
+
{
|
|
15756
|
+
input: resolvedInput,
|
|
15757
|
+
instructions,
|
|
15758
|
+
session_id: hermesSessionId,
|
|
15759
|
+
session_key: sessionKey,
|
|
15760
|
+
model: run.model,
|
|
15761
|
+
...previousResponseId ? { previous_response_id: previousResponseId } : {},
|
|
15762
|
+
...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
|
|
15763
|
+
},
|
|
15764
|
+
{
|
|
15765
|
+
logger: this.deps.logger,
|
|
15766
|
+
profileName: run.profile,
|
|
15767
|
+
signal: controller.signal
|
|
15768
|
+
}
|
|
15769
|
+
);
|
|
15770
|
+
} catch (error) {
|
|
15771
|
+
if (!previousResponseId || !isLinkHttpError(error) || error.code !== "hermes_previous_response_not_found") {
|
|
15772
|
+
throw error;
|
|
15242
15773
|
}
|
|
15243
|
-
|
|
15774
|
+
await this.deps.logger.warn(
|
|
15775
|
+
"hermes_previous_response_missing_falling_back_to_history",
|
|
15776
|
+
{
|
|
15777
|
+
conversation_id: conversationId,
|
|
15778
|
+
run_id: runId,
|
|
15779
|
+
previous_response_id: previousResponseId,
|
|
15780
|
+
error: error.message
|
|
15781
|
+
}
|
|
15782
|
+
);
|
|
15783
|
+
await this.updateRun(conversationId, runId, {
|
|
15784
|
+
previous_response_id: void 0
|
|
15785
|
+
});
|
|
15786
|
+
conversationHistory = await buildConversationHistory({
|
|
15787
|
+
paths: this.deps.paths,
|
|
15788
|
+
profileName: run.profile,
|
|
15789
|
+
hermesSessionId,
|
|
15790
|
+
snapshot,
|
|
15791
|
+
run
|
|
15792
|
+
}).catch(async (buildError) => {
|
|
15793
|
+
await this.deps.logger.warn("conversation_history_build_failed", {
|
|
15794
|
+
conversation_id: conversationId,
|
|
15795
|
+
run_id: runId,
|
|
15796
|
+
error: buildError instanceof Error ? buildError.message : String(buildError)
|
|
15797
|
+
});
|
|
15798
|
+
return {
|
|
15799
|
+
messages: [],
|
|
15800
|
+
source: "empty",
|
|
15801
|
+
diagnostics: emptyConversationHistoryDiagnostics("build_failed")
|
|
15802
|
+
};
|
|
15803
|
+
});
|
|
15804
|
+
await this.deps.logger.debug("conversation_history_built", {
|
|
15805
|
+
conversation_id: conversationId,
|
|
15806
|
+
run_id: runId,
|
|
15807
|
+
backend,
|
|
15808
|
+
source: conversationHistory.source,
|
|
15809
|
+
message_count: conversationHistory.messages.length,
|
|
15810
|
+
...conversationHistory.diagnostics
|
|
15811
|
+
});
|
|
15812
|
+
const fallbackUsage = estimateContextUsage({
|
|
15813
|
+
conversationHistory: conversationHistory.messages,
|
|
15814
|
+
currentInput: resolvedInput,
|
|
15815
|
+
instructions,
|
|
15816
|
+
contextWindow: run.context_window
|
|
15817
|
+
});
|
|
15818
|
+
await this.updateRun(conversationId, runId, { usage: fallbackUsage });
|
|
15819
|
+
response = await streamHermesResponses(
|
|
15820
|
+
{
|
|
15821
|
+
input: resolvedInput,
|
|
15822
|
+
instructions,
|
|
15823
|
+
session_id: hermesSessionId,
|
|
15824
|
+
session_key: sessionKey,
|
|
15825
|
+
model: run.model,
|
|
15826
|
+
...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
|
|
15827
|
+
},
|
|
15828
|
+
{
|
|
15829
|
+
logger: this.deps.logger,
|
|
15830
|
+
profileName: run.profile,
|
|
15831
|
+
signal: controller.signal
|
|
15832
|
+
}
|
|
15833
|
+
);
|
|
15834
|
+
}
|
|
15244
15835
|
const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
|
|
15245
15836
|
if (responseSessionId) {
|
|
15246
15837
|
await this.rememberRunHermesSessionId(
|
|
@@ -15336,6 +15927,7 @@ var ConversationRunLifecycle = class {
|
|
|
15336
15927
|
);
|
|
15337
15928
|
}
|
|
15338
15929
|
async failRun(conversationId, runId, message, source) {
|
|
15930
|
+
await this.refreshRunHermesCompressionTip(conversationId, runId);
|
|
15339
15931
|
return this.deps.withConversationLock(
|
|
15340
15932
|
conversationId,
|
|
15341
15933
|
() => this.failRunLocked(conversationId, runId, message, source)
|
|
@@ -15433,9 +16025,21 @@ var ConversationRunLifecycle = class {
|
|
|
15433
16025
|
await this.cancelRunAfterAbort(input.conversationId, input.runId);
|
|
15434
16026
|
return;
|
|
15435
16027
|
}
|
|
15436
|
-
|
|
16028
|
+
const hasAssistantOutput = await this.runHasAssistantOutput(
|
|
16029
|
+
input.conversationId,
|
|
16030
|
+
input.runId
|
|
16031
|
+
);
|
|
16032
|
+
if (input.backend === "responses" && !streamError && hasAssistantOutput) {
|
|
15437
16033
|
await this.completeRun(input.conversationId, input.runId);
|
|
15438
16034
|
} else {
|
|
16035
|
+
await this.deps.logger.warn("hermes_event_stream_ended_without_terminal", {
|
|
16036
|
+
backend: input.backend,
|
|
16037
|
+
conversation_id: input.conversationId,
|
|
16038
|
+
run_id: input.runId,
|
|
16039
|
+
...input.hermesRunId ? { hermes_run_id: input.hermesRunId } : {},
|
|
16040
|
+
has_assistant_output: hasAssistantOutput,
|
|
16041
|
+
...streamError ? { error: formatUnknownErrorMessage(streamError) } : {}
|
|
16042
|
+
});
|
|
15439
16043
|
await this.failRun(
|
|
15440
16044
|
input.conversationId,
|
|
15441
16045
|
input.runId,
|
|
@@ -16053,11 +16657,38 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
16053
16657
|
return user ? messageRequestsAppDelivery(messageText(user)) : false;
|
|
16054
16658
|
}
|
|
16055
16659
|
async completeRun(conversationId, runId, source) {
|
|
16660
|
+
await this.refreshRunHermesCompressionTip(conversationId, runId);
|
|
16056
16661
|
return this.deps.withConversationLock(
|
|
16057
16662
|
conversationId,
|
|
16058
16663
|
() => this.completeRunLocked(conversationId, runId, source)
|
|
16059
16664
|
);
|
|
16060
16665
|
}
|
|
16666
|
+
async refreshRunHermesCompressionTip(conversationId, runId) {
|
|
16667
|
+
const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
|
|
16668
|
+
const run = snapshot?.runs.find((item) => item.id === runId);
|
|
16669
|
+
if (!run?.hermes_session_id) {
|
|
16670
|
+
return;
|
|
16671
|
+
}
|
|
16672
|
+
const compressionTip = await readHermesCompressionTip(
|
|
16673
|
+
run.hermes_session_id,
|
|
16674
|
+
this.deps.paths,
|
|
16675
|
+
run.profile
|
|
16676
|
+
).catch(() => void 0);
|
|
16677
|
+
if (!compressionTip || compressionTip === run.hermes_session_id) {
|
|
16678
|
+
return;
|
|
16679
|
+
}
|
|
16680
|
+
await this.deps.logger.info("hermes_compression_tip_detected", {
|
|
16681
|
+
conversation_id: conversationId,
|
|
16682
|
+
run_id: runId,
|
|
16683
|
+
previous_hermes_session_id: run.hermes_session_id,
|
|
16684
|
+
hermes_session_id: compressionTip
|
|
16685
|
+
});
|
|
16686
|
+
await this.rememberRunHermesSessionId(
|
|
16687
|
+
conversationId,
|
|
16688
|
+
runId,
|
|
16689
|
+
compressionTip
|
|
16690
|
+
);
|
|
16691
|
+
}
|
|
16061
16692
|
async completeRunLocked(conversationId, runId, source) {
|
|
16062
16693
|
let snapshot = await this.deps.readSnapshot(conversationId);
|
|
16063
16694
|
let run = snapshot.runs.find((item) => item.id === runId);
|
|
@@ -16744,6 +17375,7 @@ var ConversationService = class {
|
|
|
16744
17375
|
metadata: this.metadata,
|
|
16745
17376
|
commandHandlers: this.commandHandlers,
|
|
16746
17377
|
runLifecycle: this.runLifecycle,
|
|
17378
|
+
logger: this.logger,
|
|
16747
17379
|
withConversationLock: (conversationId, task) => this.withConversationLock(conversationId, task),
|
|
16748
17380
|
appendEvent: (conversationId, input) => this.appendEvent(conversationId, input),
|
|
16749
17381
|
resolveMessageAttachmentParts: (conversationId, attachments) => this.maintenance.resolveMessageAttachmentParts(
|
|
@@ -17262,7 +17894,14 @@ var ConversationService = class {
|
|
|
17262
17894
|
reason: "cancelled by app"
|
|
17263
17895
|
});
|
|
17264
17896
|
if (result.run.status === "cancelled") {
|
|
17265
|
-
void this.orchestration.startNextQueuedRun(conversationId)
|
|
17897
|
+
void this.orchestration.startNextQueuedRun(conversationId).catch(
|
|
17898
|
+
(error) => {
|
|
17899
|
+
void this.logger.warn("conversation_queue_drain_failed", {
|
|
17900
|
+
conversation_id: conversationId,
|
|
17901
|
+
error: error instanceof Error ? error.message : String(error)
|
|
17902
|
+
});
|
|
17903
|
+
}
|
|
17904
|
+
);
|
|
17266
17905
|
}
|
|
17267
17906
|
return result;
|
|
17268
17907
|
}
|
|
@@ -18282,6 +18921,7 @@ function isLanHost(hostname) {
|
|
|
18282
18921
|
// src/http/sse.ts
|
|
18283
18922
|
var DEFAULT_SSE_RETRY_MS = 1e3;
|
|
18284
18923
|
var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
|
|
18924
|
+
var activeSseSockets = /* @__PURE__ */ new WeakSet();
|
|
18285
18925
|
function beginSseStream(request, response, options = {}) {
|
|
18286
18926
|
const retryMs = normalizeRetryMs(options.retryMs);
|
|
18287
18927
|
const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
|
|
@@ -18289,11 +18929,15 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18289
18929
|
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
18290
18930
|
response.setHeader("cache-control", "no-store");
|
|
18291
18931
|
response.setHeader("connection", "keep-alive");
|
|
18932
|
+
activeSseSockets.add(request.socket);
|
|
18292
18933
|
response.flushHeaders();
|
|
18293
18934
|
writeSseRetry(response, retryMs);
|
|
18294
18935
|
writeSseComment(response, options.initialComment ?? "connected");
|
|
18295
18936
|
let closed = false;
|
|
18296
18937
|
let heartbeat = null;
|
|
18938
|
+
const onStreamError = () => {
|
|
18939
|
+
cleanup();
|
|
18940
|
+
};
|
|
18297
18941
|
const cleanup = () => {
|
|
18298
18942
|
if (closed) {
|
|
18299
18943
|
return;
|
|
@@ -18305,9 +18949,18 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18305
18949
|
}
|
|
18306
18950
|
request.off("close", cleanup);
|
|
18307
18951
|
response.off("close", cleanup);
|
|
18952
|
+
request.off("error", onStreamError);
|
|
18953
|
+
response.off("error", onStreamError);
|
|
18954
|
+
activeSseSockets.delete(request.socket);
|
|
18308
18955
|
options.onClose?.();
|
|
18309
18956
|
if (!response.writableEnded && !response.destroyed) {
|
|
18310
|
-
|
|
18957
|
+
try {
|
|
18958
|
+
response.end();
|
|
18959
|
+
} catch (error) {
|
|
18960
|
+
if (!isExpectedClientDisconnectError(error)) {
|
|
18961
|
+
throw error;
|
|
18962
|
+
}
|
|
18963
|
+
}
|
|
18311
18964
|
}
|
|
18312
18965
|
};
|
|
18313
18966
|
heartbeat = setInterval(() => {
|
|
@@ -18320,37 +18973,43 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18320
18973
|
heartbeat.unref();
|
|
18321
18974
|
request.once("close", cleanup);
|
|
18322
18975
|
response.once("close", cleanup);
|
|
18976
|
+
request.once("error", onStreamError);
|
|
18977
|
+
response.once("error", onStreamError);
|
|
18323
18978
|
return cleanup;
|
|
18324
18979
|
}
|
|
18980
|
+
function isActiveSseSocket(socket) {
|
|
18981
|
+
return socket != null && activeSseSockets.has(socket);
|
|
18982
|
+
}
|
|
18325
18983
|
function writeSseEvent(response, event) {
|
|
18984
|
+
const appEvent = projectAppConversationEvent(event);
|
|
18326
18985
|
writeJsonSseEvent(response, {
|
|
18327
|
-
event:
|
|
18328
|
-
data:
|
|
18329
|
-
id:
|
|
18986
|
+
event: appEvent.type,
|
|
18987
|
+
data: appEvent,
|
|
18988
|
+
id: appEvent.seq
|
|
18330
18989
|
});
|
|
18331
18990
|
}
|
|
18332
18991
|
function writeJsonSseEvent(response, event) {
|
|
18333
18992
|
if (event.retryMs != null) {
|
|
18334
|
-
response
|
|
18993
|
+
writeResponse(response, `retry: ${normalizeRetryMs(event.retryMs)}
|
|
18335
18994
|
`);
|
|
18336
18995
|
}
|
|
18337
18996
|
if (event.id != null && event.id !== "") {
|
|
18338
|
-
response
|
|
18997
|
+
writeResponse(response, `id: ${event.id}
|
|
18339
18998
|
`);
|
|
18340
18999
|
}
|
|
18341
|
-
response
|
|
19000
|
+
writeResponse(response, `event: ${event.event}
|
|
18342
19001
|
`);
|
|
18343
|
-
response
|
|
19002
|
+
writeResponse(response, `data: ${JSON.stringify(event.data)}
|
|
18344
19003
|
|
|
18345
19004
|
`);
|
|
18346
19005
|
}
|
|
18347
19006
|
function writeSseComment(response, comment = "keep-alive") {
|
|
18348
|
-
response
|
|
19007
|
+
writeResponse(response, `: ${comment}
|
|
18349
19008
|
|
|
18350
19009
|
`);
|
|
18351
19010
|
}
|
|
18352
19011
|
function writeSseRetry(response, retryMs) {
|
|
18353
|
-
response
|
|
19012
|
+
writeResponse(response, `retry: ${normalizeRetryMs(retryMs)}
|
|
18354
19013
|
|
|
18355
19014
|
`);
|
|
18356
19015
|
}
|
|
@@ -18358,6 +19017,25 @@ function normalizeRetryMs(retryMs) {
|
|
|
18358
19017
|
const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
|
|
18359
19018
|
return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
|
|
18360
19019
|
}
|
|
19020
|
+
function writeResponse(response, chunk) {
|
|
19021
|
+
if (response.writableEnded || response.destroyed) {
|
|
19022
|
+
return;
|
|
19023
|
+
}
|
|
19024
|
+
try {
|
|
19025
|
+
response.write(chunk);
|
|
19026
|
+
} catch (error) {
|
|
19027
|
+
if (!isExpectedClientDisconnectError(error)) {
|
|
19028
|
+
throw error;
|
|
19029
|
+
}
|
|
19030
|
+
}
|
|
19031
|
+
}
|
|
19032
|
+
function isExpectedClientDisconnectError(error) {
|
|
19033
|
+
if (!(error instanceof Error)) {
|
|
19034
|
+
return false;
|
|
19035
|
+
}
|
|
19036
|
+
const code = String(error.code ?? "");
|
|
19037
|
+
return code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || code === "ETIMEDOUT" || /(?:socket hang up|aborted|write after end)/iu.test(error.message);
|
|
19038
|
+
}
|
|
18361
19039
|
|
|
18362
19040
|
// src/http/routes/conversations.ts
|
|
18363
19041
|
function registerConversationRoutes(router, options) {
|
|
@@ -18838,12 +19516,22 @@ function encodeRfc5987Value(value) {
|
|
|
18838
19516
|
}
|
|
18839
19517
|
|
|
18840
19518
|
// src/http/middleware/error-handler.ts
|
|
19519
|
+
var INTERNAL_HEALTH_PROBE_HEADER = "x-hermes-link-internal-health-probe";
|
|
18841
19520
|
function createHttpErrorMiddleware(logger) {
|
|
18842
19521
|
return async (ctx, next) => {
|
|
18843
19522
|
const startedAt = Date.now();
|
|
19523
|
+
const shouldSkipRequestLog = isInternalHealthProbe(ctx);
|
|
19524
|
+
let expectedClientDisconnect = false;
|
|
18844
19525
|
try {
|
|
18845
19526
|
await next();
|
|
18846
19527
|
} catch (error) {
|
|
19528
|
+
if (isExpectedClientDisconnectError2(error, {
|
|
19529
|
+
sse: isSseRequestContext(ctx)
|
|
19530
|
+
})) {
|
|
19531
|
+
expectedClientDisconnect = true;
|
|
19532
|
+
ctx.respond = false;
|
|
19533
|
+
return;
|
|
19534
|
+
}
|
|
18847
19535
|
const profileError = error instanceof Error && error.message === "invalid profile name";
|
|
18848
19536
|
const profileNotFound = error instanceof Error && error.message === "profile does not exist";
|
|
18849
19537
|
const status = isLinkHttpError(error) ? error.status : profileError ? 400 : profileNotFound ? 404 : 500;
|
|
@@ -18856,28 +19544,57 @@ function createHttpErrorMiddleware(logger) {
|
|
|
18856
19544
|
message: error instanceof Error ? error.message : "Internal error"
|
|
18857
19545
|
}
|
|
18858
19546
|
};
|
|
18859
|
-
|
|
18860
|
-
|
|
18861
|
-
|
|
18862
|
-
|
|
19547
|
+
if (!shouldSkipRequestLog) {
|
|
19548
|
+
void logger.write(
|
|
19549
|
+
status >= 500 ? "error" : "warn",
|
|
19550
|
+
"http_request_failed",
|
|
19551
|
+
{
|
|
19552
|
+
method: ctx.method,
|
|
19553
|
+
path: ctx.path,
|
|
19554
|
+
query: ctx.querystring || null,
|
|
19555
|
+
status,
|
|
19556
|
+
code,
|
|
19557
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19558
|
+
}
|
|
19559
|
+
);
|
|
19560
|
+
}
|
|
19561
|
+
} finally {
|
|
19562
|
+
if (!shouldSkipRequestLog && !expectedClientDisconnect) {
|
|
19563
|
+
void logger.info("http_request", {
|
|
18863
19564
|
method: ctx.method,
|
|
18864
19565
|
path: ctx.path,
|
|
18865
|
-
|
|
18866
|
-
|
|
18867
|
-
|
|
18868
|
-
|
|
18869
|
-
}
|
|
18870
|
-
);
|
|
18871
|
-
} finally {
|
|
18872
|
-
void logger.info("http_request", {
|
|
18873
|
-
method: ctx.method,
|
|
18874
|
-
path: ctx.path,
|
|
18875
|
-
status: ctx.status,
|
|
18876
|
-
duration_ms: Date.now() - startedAt
|
|
18877
|
-
});
|
|
19566
|
+
status: ctx.status,
|
|
19567
|
+
duration_ms: Date.now() - startedAt
|
|
19568
|
+
});
|
|
19569
|
+
}
|
|
18878
19570
|
}
|
|
18879
19571
|
};
|
|
18880
19572
|
}
|
|
19573
|
+
function isInternalHealthProbe(ctx) {
|
|
19574
|
+
return ctx.path === "/api/v1/bootstrap" && ctx.get(INTERNAL_HEALTH_PROBE_HEADER) === "1";
|
|
19575
|
+
}
|
|
19576
|
+
function isSseRequestContext(ctx) {
|
|
19577
|
+
if (!ctx) {
|
|
19578
|
+
return false;
|
|
19579
|
+
}
|
|
19580
|
+
return isSseRequestPath(ctx.path) || isActiveSseSocket(ctx.req.socket);
|
|
19581
|
+
}
|
|
19582
|
+
function isSseRequestPath(path29) {
|
|
19583
|
+
if (!path29) {
|
|
19584
|
+
return false;
|
|
19585
|
+
}
|
|
19586
|
+
return path29 === "/api/v1/conversations/events" || path29 === "/api/v1/profile-creation/events" || path29 === "/api/v1/hermes/update/events" || path29 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path29) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path29);
|
|
19587
|
+
}
|
|
19588
|
+
function isExpectedClientDisconnectError2(error, options = {}) {
|
|
19589
|
+
if (!(error instanceof Error)) {
|
|
19590
|
+
return false;
|
|
19591
|
+
}
|
|
19592
|
+
const code = String(error.code ?? "");
|
|
19593
|
+
if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted|write after end)/iu.test(error.message)) {
|
|
19594
|
+
return true;
|
|
19595
|
+
}
|
|
19596
|
+
return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
|
|
19597
|
+
}
|
|
18881
19598
|
|
|
18882
19599
|
// src/hermes/profiles.ts
|
|
18883
19600
|
import { execFile as execFile4 } from "child_process";
|
|
@@ -19855,7 +20572,12 @@ function readModelConfigInput(body) {
|
|
|
19855
20572
|
function readModelDefaultsInput(body) {
|
|
19856
20573
|
return {
|
|
19857
20574
|
taskModelId: readString16(body, "task_model_id") ?? readString16(body, "taskModelId") ?? readString16(body, "default_model_id") ?? readString16(body, "defaultModelId") ?? void 0,
|
|
19858
|
-
|
|
20575
|
+
taskModelProvider: readString16(body, "task_model_provider") ?? readString16(body, "taskModelProvider") ?? readString16(body, "default_model_provider") ?? readString16(body, "defaultModelProvider") ?? void 0,
|
|
20576
|
+
taskModelBaseUrl: readString16(body, "task_model_base_url") ?? readString16(body, "taskModelBaseUrl") ?? readString16(body, "default_model_base_url") ?? readString16(body, "defaultModelBaseUrl") ?? void 0,
|
|
20577
|
+
compressionModelId: readString16(body, "compression_model_id") ?? readString16(body, "compressionModelId") ?? void 0,
|
|
20578
|
+
compressionModelProvider: readString16(body, "compression_model_provider") ?? readString16(body, "compressionModelProvider") ?? void 0,
|
|
20579
|
+
compressionModelBaseUrl: readString16(body, "compression_model_base_url") ?? readString16(body, "compressionModelBaseUrl") ?? void 0,
|
|
20580
|
+
reasoningEffort: readString16(body, "reasoning_effort") ?? readString16(body, "reasoningEffort") ?? readString16(body, "default_reasoning_effort") ?? readString16(body, "defaultReasoningEffort") ?? void 0
|
|
19859
20581
|
};
|
|
19860
20582
|
}
|
|
19861
20583
|
function readModelConfigImportInput(body) {
|
|
@@ -19958,85 +20680,392 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
19958
20680
|
import {
|
|
19959
20681
|
cp,
|
|
19960
20682
|
mkdir as mkdir11,
|
|
19961
|
-
readFile as
|
|
20683
|
+
readFile as readFile15,
|
|
19962
20684
|
rm as rm6,
|
|
19963
|
-
stat as
|
|
20685
|
+
stat as stat15
|
|
19964
20686
|
} from "fs/promises";
|
|
20687
|
+
import path22 from "path";
|
|
20688
|
+
import YAML4 from "yaml";
|
|
20689
|
+
|
|
20690
|
+
// src/hermes/link-skill.ts
|
|
20691
|
+
import { readFile as readFile14, stat as stat14 } from "fs/promises";
|
|
20692
|
+
import os4 from "os";
|
|
19965
20693
|
import path21 from "path";
|
|
19966
20694
|
import YAML3 from "yaml";
|
|
19967
|
-
var
|
|
19968
|
-
var
|
|
19969
|
-
var
|
|
19970
|
-
var
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
20695
|
+
var HERMES_LINK_SKILL_ROOT_DIR = "hermes-skills";
|
|
20696
|
+
var HERMES_LINK_SKILL_DIR = "hermes-link";
|
|
20697
|
+
var HERMES_LINK_SKILL_FILE = "SKILL.md";
|
|
20698
|
+
var HERMES_LINK_SKILL_CONTENT = `---
|
|
20699
|
+
name: hermes-link
|
|
20700
|
+
description: Understand and troubleshoot Hermes Link, the local companion service that connects Hermes Agent to the HermesPilot mobile app.
|
|
20701
|
+
---
|
|
20702
|
+
|
|
20703
|
+
# Hermes Link
|
|
20704
|
+
|
|
20705
|
+
Hermes Link is a secure local companion service for hermes-agent. It lets the HermesPilot mobile app connect to the user's local Hermes Agent safely and reliably.
|
|
20706
|
+
|
|
20707
|
+
Hermes Link is specifically for HermesPilot. It is not OpenClaw, ClawPilot, clawlink, or any OpenClaw companion service. Do not confuse those systems when helping the user.
|
|
20708
|
+
|
|
20709
|
+
## What Hermes Link Does
|
|
20710
|
+
|
|
20711
|
+
Hermes Link can:
|
|
20712
|
+
|
|
20713
|
+
- pair the HermesPilot mobile app with this computer
|
|
20714
|
+
- expose a controlled local API for the app
|
|
20715
|
+
- connect through LAN, public direct routes, or Hermes Relay
|
|
20716
|
+
- help the app access Hermes profiles, conversations, messages, files, voice input, logs, and status
|
|
20717
|
+
- start or check the Hermes Gateway when needed
|
|
20718
|
+
|
|
20719
|
+
Hermes Link does not replace Hermes Agent. Hermes Agent remains the actual AI agent runtime.
|
|
20720
|
+
|
|
20721
|
+
## When To Use This Skill
|
|
20722
|
+
|
|
20723
|
+
Use this skill when the user asks about:
|
|
20724
|
+
|
|
20725
|
+
- HermesPilot App cannot connect
|
|
20726
|
+
- Link appears offline
|
|
20727
|
+
- pairing or QR code problems
|
|
20728
|
+
- mobile access to local Hermes
|
|
20729
|
+
- Relay, LAN, or public direct route problems
|
|
20730
|
+
- Hermes Link logs, daemon status, or diagnostics
|
|
20731
|
+
|
|
20732
|
+
## Useful Commands
|
|
20733
|
+
|
|
20734
|
+
Start with:
|
|
20735
|
+
|
|
20736
|
+
\`\`\`bash
|
|
20737
|
+
hermeslink status
|
|
20738
|
+
hermeslink doctor
|
|
20739
|
+
\`\`\`
|
|
20740
|
+
|
|
20741
|
+
For logs:
|
|
20742
|
+
|
|
20743
|
+
\`\`\`bash
|
|
20744
|
+
hermeslink logs --error -n 50
|
|
20745
|
+
hermeslink logs --warn -n 100
|
|
20746
|
+
hermeslink logs -f
|
|
20747
|
+
hermeslink logs --all --level debug -f
|
|
20748
|
+
\`\`\`
|
|
20749
|
+
|
|
20750
|
+
If the daemon appears stuck, suggest:
|
|
20751
|
+
|
|
20752
|
+
\`\`\`bash
|
|
20753
|
+
hermeslink restart
|
|
20754
|
+
\`\`\`
|
|
20755
|
+
|
|
20756
|
+
Explain that restarting Hermes Link may briefly disconnect the mobile app.
|
|
20757
|
+
|
|
20758
|
+
## Troubleshooting Flow
|
|
20759
|
+
|
|
20760
|
+
1. Check \`hermeslink status\`.
|
|
20761
|
+
2. Run \`hermeslink doctor\`.
|
|
20762
|
+
3. Inspect recent errors with \`hermeslink logs --error -n 50\`.
|
|
20763
|
+
4. If the issue is intermittent, reproduce it while running \`hermeslink logs -f\`.
|
|
20764
|
+
5. Check whether the phone and computer are on the same LAN, using Relay, or using a public direct route.
|
|
20765
|
+
6. If the user uses WSL, Docker, or a VM, verify that Hermes Agent and Hermes Link run in the same environment.
|
|
20766
|
+
|
|
20767
|
+
## Safety
|
|
20768
|
+
|
|
20769
|
+
Never reveal API keys, access tokens, refresh tokens, private keys, or full .env contents.
|
|
20770
|
+
|
|
20771
|
+
Do not recommend exposing port 52379 directly to the public internet without TLS, VPN, Tailscale, WireGuard, or another access-control layer.
|
|
20772
|
+
|
|
20773
|
+
Do not modify Hermes profiles, delete user data, edit config files, or kill processes unless the user explicitly asks.
|
|
20774
|
+
`;
|
|
20775
|
+
async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
|
|
20776
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
20777
|
+
const externalDir = resolveHermesLinkSkillExternalDir(paths);
|
|
20778
|
+
const skillPath = path21.join(
|
|
20779
|
+
externalDir,
|
|
20780
|
+
HERMES_LINK_SKILL_DIR,
|
|
20781
|
+
HERMES_LINK_SKILL_FILE
|
|
20782
|
+
);
|
|
20783
|
+
const skillChanged = await writeHermesLinkSkill(skillPath);
|
|
20784
|
+
const profiles = await listHermesProfiles(paths);
|
|
20785
|
+
const results = [];
|
|
20786
|
+
for (const profile of profiles) {
|
|
20787
|
+
try {
|
|
20788
|
+
results.push(await ensureProfileUsesExternalSkillDir(profile, externalDir));
|
|
20789
|
+
} catch (error) {
|
|
20790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20791
|
+
results.push({
|
|
20792
|
+
profile: profile.name,
|
|
20793
|
+
profilePath: profile.path,
|
|
20794
|
+
configPath: profile.configPath,
|
|
20795
|
+
changed: false,
|
|
20796
|
+
backupPath: null,
|
|
20797
|
+
skipped: false,
|
|
20798
|
+
error: message
|
|
20799
|
+
});
|
|
20800
|
+
}
|
|
19984
20801
|
}
|
|
19985
|
-
const
|
|
19986
|
-
const
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
error: null
|
|
20802
|
+
const changedProfiles = results.filter((result) => result.changed);
|
|
20803
|
+
const failedProfiles = results.filter((result) => result.error);
|
|
20804
|
+
if (skillChanged || changedProfiles.length > 0) {
|
|
20805
|
+
void options.logger?.info("hermes_link_skill_ensured", {
|
|
20806
|
+
source: options.source ?? "unspecified",
|
|
20807
|
+
external_dir: externalDir,
|
|
20808
|
+
skill_path: skillPath,
|
|
20809
|
+
skill_changed: skillChanged,
|
|
20810
|
+
changed_profiles: changedProfiles.map((result) => result.profile),
|
|
20811
|
+
failed_profiles: failedProfiles.map((result) => result.profile)
|
|
20812
|
+
});
|
|
20813
|
+
}
|
|
20814
|
+
if (failedProfiles.length > 0) {
|
|
20815
|
+
void options.logger?.warn("hermes_link_skill_profile_ensure_failed", {
|
|
20816
|
+
source: options.source ?? "unspecified",
|
|
20817
|
+
profiles: failedProfiles.map((result) => ({
|
|
20818
|
+
profile: result.profile,
|
|
20819
|
+
error: result.error ?? "unknown error"
|
|
20820
|
+
}))
|
|
20821
|
+
});
|
|
20822
|
+
}
|
|
20823
|
+
return {
|
|
20824
|
+
externalDir,
|
|
20825
|
+
skillPath,
|
|
20826
|
+
skillChanged,
|
|
20827
|
+
profiles: results
|
|
20012
20828
|
};
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
`Copy from: ${sourceProfile} (${copyScopes.join(", ")})
|
|
20023
|
-
`
|
|
20024
|
-
);
|
|
20025
|
-
} else {
|
|
20026
|
-
await writer.write("Copy from: none\n");
|
|
20829
|
+
}
|
|
20830
|
+
async function ensureHermesLinkSkillInstalledBestEffort(options = {}) {
|
|
20831
|
+
try {
|
|
20832
|
+
await ensureHermesLinkSkillInstalledForProfiles(options);
|
|
20833
|
+
} catch (error) {
|
|
20834
|
+
void options.logger?.warn("hermes_link_skill_ensure_failed", {
|
|
20835
|
+
source: options.source ?? "unspecified",
|
|
20836
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20837
|
+
});
|
|
20027
20838
|
}
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20839
|
+
}
|
|
20840
|
+
function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
|
|
20841
|
+
return path21.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
|
|
20842
|
+
}
|
|
20843
|
+
async function writeHermesLinkSkill(skillPath) {
|
|
20844
|
+
const existing = await readFile14(skillPath, "utf8").catch((error) => {
|
|
20845
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20846
|
+
return null;
|
|
20847
|
+
}
|
|
20848
|
+
throw error;
|
|
20037
20849
|
});
|
|
20038
|
-
|
|
20039
|
-
|
|
20850
|
+
if (existing === HERMES_LINK_SKILL_CONTENT) {
|
|
20851
|
+
return false;
|
|
20852
|
+
}
|
|
20853
|
+
await atomicWriteFilePreservingMetadata(skillPath, HERMES_LINK_SKILL_CONTENT);
|
|
20854
|
+
return true;
|
|
20855
|
+
}
|
|
20856
|
+
async function ensureProfileUsesExternalSkillDir(profile, externalDir) {
|
|
20857
|
+
const profilePath = resolveHermesProfileDir(profile.name);
|
|
20858
|
+
const configPath = resolveHermesConfigPath(profile.name);
|
|
20859
|
+
if (!await pathIsDirectory(profilePath)) {
|
|
20860
|
+
return {
|
|
20861
|
+
profile: profile.name,
|
|
20862
|
+
profilePath,
|
|
20863
|
+
configPath,
|
|
20864
|
+
changed: false,
|
|
20865
|
+
backupPath: null,
|
|
20866
|
+
skipped: true
|
|
20867
|
+
};
|
|
20868
|
+
}
|
|
20869
|
+
const { document, config, existingRaw } = await readHermesConfigDocument2(configPath);
|
|
20870
|
+
const skillsConfig = ensureRecord2(config, "skills");
|
|
20871
|
+
if (externalDirsInclude(skillsConfig.external_dirs, externalDir, profilePath)) {
|
|
20872
|
+
return {
|
|
20873
|
+
profile: profile.name,
|
|
20874
|
+
profilePath,
|
|
20875
|
+
configPath,
|
|
20876
|
+
changed: false,
|
|
20877
|
+
backupPath: null,
|
|
20878
|
+
skipped: false
|
|
20879
|
+
};
|
|
20880
|
+
}
|
|
20881
|
+
skillsConfig.external_dirs = appendExternalDir(
|
|
20882
|
+
skillsConfig.external_dirs,
|
|
20883
|
+
externalDir,
|
|
20884
|
+
profilePath
|
|
20885
|
+
);
|
|
20886
|
+
const backupPath = await writeHermesConfigDocument2({
|
|
20887
|
+
configPath,
|
|
20888
|
+
document,
|
|
20889
|
+
config,
|
|
20890
|
+
existingRaw
|
|
20891
|
+
});
|
|
20892
|
+
return {
|
|
20893
|
+
profile: profile.name,
|
|
20894
|
+
profilePath,
|
|
20895
|
+
configPath,
|
|
20896
|
+
changed: true,
|
|
20897
|
+
backupPath,
|
|
20898
|
+
skipped: false
|
|
20899
|
+
};
|
|
20900
|
+
}
|
|
20901
|
+
async function readHermesConfigDocument2(configPath) {
|
|
20902
|
+
const existingRaw = await readFile14(configPath, "utf8").catch(
|
|
20903
|
+
(error) => {
|
|
20904
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20905
|
+
return null;
|
|
20906
|
+
}
|
|
20907
|
+
throw error;
|
|
20908
|
+
}
|
|
20909
|
+
);
|
|
20910
|
+
const document = existingRaw ? YAML3.parseDocument(existingRaw) : new YAML3.Document({});
|
|
20911
|
+
return {
|
|
20912
|
+
document,
|
|
20913
|
+
config: toRecord15(document.toJSON()),
|
|
20914
|
+
existingRaw
|
|
20915
|
+
};
|
|
20916
|
+
}
|
|
20917
|
+
async function writeHermesConfigDocument2(input) {
|
|
20918
|
+
const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
|
|
20919
|
+
if (backupPath) {
|
|
20920
|
+
await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
|
|
20921
|
+
metadataSourcePath: input.configPath
|
|
20922
|
+
});
|
|
20923
|
+
}
|
|
20924
|
+
input.document.contents = input.document.createNode(input.config);
|
|
20925
|
+
await atomicWriteFilePreservingMetadata(
|
|
20926
|
+
input.configPath,
|
|
20927
|
+
input.document.toString()
|
|
20928
|
+
);
|
|
20929
|
+
return backupPath;
|
|
20930
|
+
}
|
|
20931
|
+
function appendExternalDir(current, externalDir, hermesHome) {
|
|
20932
|
+
const entries = readExternalDirEntries(current);
|
|
20933
|
+
const seen = new Set(
|
|
20934
|
+
entries.map((entry) => resolveExternalDirEntry(entry, hermesHome))
|
|
20935
|
+
);
|
|
20936
|
+
const normalizedExternalDir = path21.resolve(externalDir);
|
|
20937
|
+
return seen.has(normalizedExternalDir) ? entries : [...entries, normalizedExternalDir];
|
|
20938
|
+
}
|
|
20939
|
+
function externalDirsInclude(current, externalDir, hermesHome) {
|
|
20940
|
+
const normalizedExternalDir = path21.resolve(externalDir);
|
|
20941
|
+
return readExternalDirEntries(current).some(
|
|
20942
|
+
(entry) => resolveExternalDirEntry(entry, hermesHome) === normalizedExternalDir
|
|
20943
|
+
);
|
|
20944
|
+
}
|
|
20945
|
+
function readExternalDirEntries(value) {
|
|
20946
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
20947
|
+
return raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
|
|
20948
|
+
}
|
|
20949
|
+
function resolveExternalDirEntry(entry, hermesHome) {
|
|
20950
|
+
const expanded = expandHome(expandEnvVars(entry));
|
|
20951
|
+
return path21.resolve(path21.isAbsolute(expanded) ? expanded : path21.join(hermesHome, expanded));
|
|
20952
|
+
}
|
|
20953
|
+
function expandHome(value) {
|
|
20954
|
+
if (value === "~") {
|
|
20955
|
+
return os4.homedir();
|
|
20956
|
+
}
|
|
20957
|
+
if (value.startsWith(`~${path21.sep}`) || value.startsWith("~/")) {
|
|
20958
|
+
return path21.join(os4.homedir(), value.slice(2));
|
|
20959
|
+
}
|
|
20960
|
+
return value;
|
|
20961
|
+
}
|
|
20962
|
+
function expandEnvVars(value) {
|
|
20963
|
+
return value.replace(
|
|
20964
|
+
/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/gu,
|
|
20965
|
+
(_match, braced, bare) => process.env[braced ?? bare ?? ""] ?? ""
|
|
20966
|
+
);
|
|
20967
|
+
}
|
|
20968
|
+
async function pathIsDirectory(filePath) {
|
|
20969
|
+
return stat14(filePath).then((value) => value.isDirectory()).catch((error) => {
|
|
20970
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20971
|
+
return false;
|
|
20972
|
+
}
|
|
20973
|
+
throw error;
|
|
20974
|
+
});
|
|
20975
|
+
}
|
|
20976
|
+
function ensureRecord2(target, key) {
|
|
20977
|
+
const existing = target[key];
|
|
20978
|
+
if (isRecord3(existing)) {
|
|
20979
|
+
return existing;
|
|
20980
|
+
}
|
|
20981
|
+
const next = {};
|
|
20982
|
+
target[key] = next;
|
|
20983
|
+
return next;
|
|
20984
|
+
}
|
|
20985
|
+
function toRecord15(value) {
|
|
20986
|
+
return isRecord3(value) ? value : {};
|
|
20987
|
+
}
|
|
20988
|
+
function isRecord3(value) {
|
|
20989
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20990
|
+
}
|
|
20991
|
+
function isNodeError16(error, code) {
|
|
20992
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
20993
|
+
}
|
|
20994
|
+
|
|
20995
|
+
// src/hermes/profile-creation.ts
|
|
20996
|
+
var PROFILE_CREATE_LOG_FILE = "profile-create.log";
|
|
20997
|
+
var PROFILE_CREATE_LOG_MAX_FILES = 3;
|
|
20998
|
+
var MAX_PROFILE_CREATE_LOG_LINES = 260;
|
|
20999
|
+
var MAX_OUTPUT_LINE_LENGTH = 1200;
|
|
21000
|
+
var PROFILE_NAME_PATTERN5 = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
|
|
21001
|
+
var ALL_COPY_SCOPES = [
|
|
21002
|
+
"models",
|
|
21003
|
+
"skills",
|
|
21004
|
+
"tool_permissions",
|
|
21005
|
+
"approval_policy"
|
|
21006
|
+
];
|
|
21007
|
+
var PROFILE_CREATION_EVENTS = new EventEmitter2();
|
|
21008
|
+
var runningProfileCreation = null;
|
|
21009
|
+
async function startHermesProfileCreation(input, options) {
|
|
21010
|
+
const current = await readHermesProfileCreationStatus(options.paths);
|
|
21011
|
+
if (runningProfileCreation || current.state === "running") {
|
|
21012
|
+
return current;
|
|
21013
|
+
}
|
|
21014
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
21015
|
+
const normalized = await normalizeProfileCreationInput(input, options.paths);
|
|
21016
|
+
const profileName = normalized.name ?? await generateProfileName(normalized.displayName, options.paths);
|
|
21017
|
+
const sourceProfile = normalized.copyFrom;
|
|
21018
|
+
const copyScopes = normalized.copyScopes;
|
|
21019
|
+
const jobId = `profile_create_${now().getTime().toString(36)}`;
|
|
21020
|
+
await clearProfileCreationLogFiles(options.paths);
|
|
21021
|
+
const writer = createRotatingTextLogWriter({
|
|
21022
|
+
paths: options.paths,
|
|
21023
|
+
fileName: PROFILE_CREATE_LOG_FILE,
|
|
21024
|
+
maxFileBytes: 512 * 1024,
|
|
21025
|
+
maxFiles: PROFILE_CREATE_LOG_MAX_FILES
|
|
21026
|
+
});
|
|
21027
|
+
const startedAt = now().toISOString();
|
|
21028
|
+
const started = {
|
|
21029
|
+
state: "running",
|
|
21030
|
+
job_id: jobId,
|
|
21031
|
+
profile_name: profileName,
|
|
21032
|
+
source_profile: sourceProfile,
|
|
21033
|
+
copied_scopes: [],
|
|
21034
|
+
profile: null,
|
|
21035
|
+
pid: null,
|
|
21036
|
+
started_at: startedAt,
|
|
21037
|
+
finished_at: null,
|
|
21038
|
+
exit_code: null,
|
|
21039
|
+
signal: null,
|
|
21040
|
+
error: null
|
|
21041
|
+
};
|
|
21042
|
+
await mkdir11(options.paths.runDir, { recursive: true, mode: 448 });
|
|
21043
|
+
await writeProfileCreationState(options.paths, started);
|
|
21044
|
+
await writer.write(`
|
|
21045
|
+
=== profile creation started ${startedAt} ===
|
|
21046
|
+
`);
|
|
21047
|
+
await writer.write(`Profile ID: ${profileName}
|
|
21048
|
+
`);
|
|
21049
|
+
if (sourceProfile && copyScopes.length > 0) {
|
|
21050
|
+
await writer.write(
|
|
21051
|
+
`Copy from: ${sourceProfile} (${copyScopes.join(", ")})
|
|
21052
|
+
`
|
|
21053
|
+
);
|
|
21054
|
+
} else {
|
|
21055
|
+
await writer.write("Copy from: none\n");
|
|
21056
|
+
}
|
|
21057
|
+
const commandArgs = ["profile", "create", profileName, "--no-alias"];
|
|
21058
|
+
await writer.write(`$ ${resolveHermesBin()} ${commandArgs.join(" ")}
|
|
21059
|
+
`);
|
|
21060
|
+
await emitProfileCreationStatus(options.paths);
|
|
21061
|
+
const child = spawn2(resolveHermesBin(), commandArgs, {
|
|
21062
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21063
|
+
env: { ...process.env, HERMES_NONINTERACTIVE: "1" },
|
|
21064
|
+
windowsHide: true,
|
|
21065
|
+
detached: false
|
|
21066
|
+
});
|
|
21067
|
+
started.pid = child.pid ?? null;
|
|
21068
|
+
await writeProfileCreationState(options.paths, started);
|
|
20040
21069
|
await emitProfileCreationStatus(options.paths);
|
|
20041
21070
|
const appendChunk = async (chunk) => {
|
|
20042
21071
|
await writer.write(chunk);
|
|
@@ -20300,6 +21329,11 @@ async function applyProfileCreationPostSteps(input) {
|
|
|
20300
21329
|
await input.writer.write("Ensuring Hermes API Server config...\n");
|
|
20301
21330
|
await ensureHermesApiServerKey(input.profileName);
|
|
20302
21331
|
await getHermesProfileStatus(input.profileName, input.paths);
|
|
21332
|
+
await input.writer.write("Ensuring Hermes Link skill...\n");
|
|
21333
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
21334
|
+
paths: input.paths,
|
|
21335
|
+
source: "profile_creation"
|
|
21336
|
+
});
|
|
20303
21337
|
if (input.displayName || input.description || input.avatarType === "url" || input.avatarUrl) {
|
|
20304
21338
|
await input.writer.write("Saving Profile display metadata...\n");
|
|
20305
21339
|
await updateProfileMetadata(input.paths, {
|
|
@@ -20352,23 +21386,23 @@ function copyModelConfig(source, target) {
|
|
|
20352
21386
|
copied[key] = cloneJson(source[key]);
|
|
20353
21387
|
}
|
|
20354
21388
|
}
|
|
20355
|
-
const sourceAuxiliary =
|
|
21389
|
+
const sourceAuxiliary = toRecord16(source.auxiliary);
|
|
20356
21390
|
if (Object.prototype.hasOwnProperty.call(sourceAuxiliary, "compression")) {
|
|
20357
|
-
const targetAuxiliary =
|
|
21391
|
+
const targetAuxiliary = ensureRecord3(target, "auxiliary");
|
|
20358
21392
|
targetAuxiliary.compression = cloneJson(sourceAuxiliary.compression);
|
|
20359
21393
|
copied.auxiliary = { compression: cloneJson(sourceAuxiliary.compression) };
|
|
20360
21394
|
}
|
|
20361
21395
|
return copied;
|
|
20362
21396
|
}
|
|
20363
21397
|
function copyToolPermissionsConfig(source, target) {
|
|
20364
|
-
const sourcePlatformToolsets =
|
|
21398
|
+
const sourcePlatformToolsets = toRecord16(source.platform_toolsets);
|
|
20365
21399
|
if (Object.prototype.hasOwnProperty.call(sourcePlatformToolsets, "api_server")) {
|
|
20366
|
-
const targetPlatformToolsets =
|
|
21400
|
+
const targetPlatformToolsets = ensureRecord3(target, "platform_toolsets");
|
|
20367
21401
|
targetPlatformToolsets.api_server = cloneJson(sourcePlatformToolsets.api_server);
|
|
20368
21402
|
}
|
|
20369
|
-
const sourceStt =
|
|
21403
|
+
const sourceStt = toRecord16(source.stt);
|
|
20370
21404
|
if (Object.prototype.hasOwnProperty.call(sourceStt, "enabled")) {
|
|
20371
|
-
const targetStt =
|
|
21405
|
+
const targetStt = ensureRecord3(target, "stt");
|
|
20372
21406
|
targetStt.enabled = cloneJson(sourceStt.enabled);
|
|
20373
21407
|
}
|
|
20374
21408
|
copyProperty(source, target, "command_allowlist");
|
|
@@ -20413,9 +21447,9 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
|
|
|
20413
21447
|
return keys;
|
|
20414
21448
|
}
|
|
20415
21449
|
async function writeEnvValues(profileName, values) {
|
|
20416
|
-
const envPath =
|
|
20417
|
-
const existingRaw = await
|
|
20418
|
-
if (
|
|
21450
|
+
const envPath = path22.join(resolveHermesProfileDir(profileName), ".env");
|
|
21451
|
+
const existingRaw = await readFile15(envPath, "utf8").catch((error) => {
|
|
21452
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20419
21453
|
return "";
|
|
20420
21454
|
}
|
|
20421
21455
|
throw error;
|
|
@@ -20450,8 +21484,8 @@ async function writeEnvValues(profileName, values) {
|
|
|
20450
21484
|
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
20451
21485
|
}
|
|
20452
21486
|
async function copySkills(sourceProfile, targetProfile) {
|
|
20453
|
-
const sourceSkills =
|
|
20454
|
-
const targetSkills =
|
|
21487
|
+
const sourceSkills = path22.join(resolveHermesProfileDir(sourceProfile), "skills");
|
|
21488
|
+
const targetSkills = path22.join(resolveHermesProfileDir(targetProfile), "skills");
|
|
20455
21489
|
if (!await pathExists2(sourceSkills)) {
|
|
20456
21490
|
return;
|
|
20457
21491
|
}
|
|
@@ -20474,16 +21508,16 @@ function copyProperty(source, target, key) {
|
|
|
20474
21508
|
}
|
|
20475
21509
|
}
|
|
20476
21510
|
async function readYamlConfig(configPath) {
|
|
20477
|
-
const existingRaw = await
|
|
21511
|
+
const existingRaw = await readFile15(configPath, "utf8").catch(
|
|
20478
21512
|
(error) => {
|
|
20479
|
-
if (
|
|
21513
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20480
21514
|
return null;
|
|
20481
21515
|
}
|
|
20482
21516
|
throw error;
|
|
20483
21517
|
}
|
|
20484
21518
|
);
|
|
20485
21519
|
return {
|
|
20486
|
-
config:
|
|
21520
|
+
config: toRecord16(existingRaw ? YAML4.parse(existingRaw) : {}),
|
|
20487
21521
|
existingRaw
|
|
20488
21522
|
};
|
|
20489
21523
|
}
|
|
@@ -20495,7 +21529,7 @@ async function writeYamlConfig(configPath, input) {
|
|
|
20495
21529
|
{ metadataSourcePath: configPath }
|
|
20496
21530
|
);
|
|
20497
21531
|
}
|
|
20498
|
-
const document = new
|
|
21532
|
+
const document = new YAML4.Document(input.config);
|
|
20499
21533
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
20500
21534
|
}
|
|
20501
21535
|
async function failProfileCreation(input) {
|
|
@@ -20537,7 +21571,7 @@ async function writeProfileCreationState(paths, state) {
|
|
|
20537
21571
|
await writeJsonFile(profileCreationStatePath(paths), state);
|
|
20538
21572
|
}
|
|
20539
21573
|
async function readProfileCreationLogLines(paths) {
|
|
20540
|
-
const raw = await
|
|
21574
|
+
const raw = await readFile15(profileCreationLogPath(paths), "utf8").catch(() => "");
|
|
20541
21575
|
if (!raw.trim()) {
|
|
20542
21576
|
return [];
|
|
20543
21577
|
}
|
|
@@ -20546,10 +21580,10 @@ async function readProfileCreationLogLines(paths) {
|
|
|
20546
21580
|
);
|
|
20547
21581
|
}
|
|
20548
21582
|
function profileCreationStatePath(paths) {
|
|
20549
|
-
return
|
|
21583
|
+
return path22.join(paths.runDir, "profile-create-state.json");
|
|
20550
21584
|
}
|
|
20551
21585
|
function profileCreationLogPath(paths) {
|
|
20552
|
-
return
|
|
21586
|
+
return path22.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
|
|
20553
21587
|
}
|
|
20554
21588
|
async function clearProfileCreationLogFiles(paths) {
|
|
20555
21589
|
const primary = profileCreationLogPath(paths);
|
|
@@ -20562,8 +21596,8 @@ async function clearProfileCreationLogFiles(paths) {
|
|
|
20562
21596
|
]);
|
|
20563
21597
|
}
|
|
20564
21598
|
async function pathExists2(targetPath) {
|
|
20565
|
-
return await
|
|
20566
|
-
if (
|
|
21599
|
+
return await stat15(targetPath).then(() => true).catch((error) => {
|
|
21600
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20567
21601
|
return false;
|
|
20568
21602
|
}
|
|
20569
21603
|
throw error;
|
|
@@ -20586,7 +21620,7 @@ function isProcessAlive(pid) {
|
|
|
20586
21620
|
return false;
|
|
20587
21621
|
}
|
|
20588
21622
|
}
|
|
20589
|
-
function
|
|
21623
|
+
function ensureRecord3(target, key) {
|
|
20590
21624
|
const value = target[key];
|
|
20591
21625
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
20592
21626
|
return value;
|
|
@@ -20595,7 +21629,7 @@ function ensureRecord2(target, key) {
|
|
|
20595
21629
|
target[key] = next;
|
|
20596
21630
|
return next;
|
|
20597
21631
|
}
|
|
20598
|
-
function
|
|
21632
|
+
function toRecord16(value) {
|
|
20599
21633
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
20600
21634
|
}
|
|
20601
21635
|
function cloneJson(value) {
|
|
@@ -20610,7 +21644,7 @@ function formatEnvValue2(value) {
|
|
|
20610
21644
|
function escapeRegExp3(value) {
|
|
20611
21645
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
20612
21646
|
}
|
|
20613
|
-
function
|
|
21647
|
+
function isNodeError17(error, code) {
|
|
20614
21648
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
20615
21649
|
}
|
|
20616
21650
|
|
|
@@ -20820,11 +21854,11 @@ function toProfileToolConfigHttpError(error) {
|
|
|
20820
21854
|
import {
|
|
20821
21855
|
access as access3,
|
|
20822
21856
|
readdir as readdir10,
|
|
20823
|
-
readFile as
|
|
20824
|
-
stat as
|
|
21857
|
+
readFile as readFile16,
|
|
21858
|
+
stat as stat16
|
|
20825
21859
|
} from "fs/promises";
|
|
20826
|
-
import
|
|
20827
|
-
import
|
|
21860
|
+
import path23 from "path";
|
|
21861
|
+
import YAML5 from "yaml";
|
|
20828
21862
|
var ENTRY_DELIMITER = "\n\xA7\n";
|
|
20829
21863
|
var DEFAULT_MEMORY_LIMIT = 2200;
|
|
20830
21864
|
var DEFAULT_USER_LIMIT = 1375;
|
|
@@ -21145,7 +22179,7 @@ async function saveProviderSettings(profileName, provider, patch) {
|
|
|
21145
22179
|
});
|
|
21146
22180
|
await patchJsonProviderConfig(
|
|
21147
22181
|
profileName,
|
|
21148
|
-
|
|
22182
|
+
path23.join("hindsight", "config.json"),
|
|
21149
22183
|
{
|
|
21150
22184
|
mode: patch.mode,
|
|
21151
22185
|
api_url: patch.apiUrl,
|
|
@@ -21202,7 +22236,7 @@ async function patchCustomProviderConfig(profileName, provider, patch) {
|
|
|
21202
22236
|
"\u81EA\u5B9A\u4E49 memory provider \u914D\u7F6E\u5FC5\u987B\u662F\u6709\u6548\u7684 JSON object\u3002"
|
|
21203
22237
|
);
|
|
21204
22238
|
}
|
|
21205
|
-
const config =
|
|
22239
|
+
const config = toRecord17(parsed);
|
|
21206
22240
|
if (Object.keys(config).length === 0 && parsed !== null) {
|
|
21207
22241
|
throw new HermesMemoryError(
|
|
21208
22242
|
"memory_provider_config_invalid",
|
|
@@ -21293,17 +22327,17 @@ function normalizeCustomProviderId(provider) {
|
|
|
21293
22327
|
}
|
|
21294
22328
|
async function patchHermesMemoryProvider(profileName, provider) {
|
|
21295
22329
|
const configPath = resolveHermesConfigPath(profileName);
|
|
21296
|
-
const existingRaw = await
|
|
22330
|
+
const existingRaw = await readFile16(configPath, "utf8").catch(
|
|
21297
22331
|
(error) => {
|
|
21298
|
-
if (
|
|
22332
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21299
22333
|
return null;
|
|
21300
22334
|
}
|
|
21301
22335
|
throw error;
|
|
21302
22336
|
}
|
|
21303
22337
|
);
|
|
21304
|
-
const document = existingRaw ?
|
|
21305
|
-
const config =
|
|
21306
|
-
const memory =
|
|
22338
|
+
const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
|
|
22339
|
+
const config = toRecord17(document.toJSON());
|
|
22340
|
+
const memory = toRecord17(config.memory);
|
|
21307
22341
|
memory.provider = provider === "built-in" ? "" : provider;
|
|
21308
22342
|
config.memory = memory;
|
|
21309
22343
|
const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
|
|
@@ -21316,13 +22350,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
|
|
|
21316
22350
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
21317
22351
|
}
|
|
21318
22352
|
function resolveMemoryDir(profileName) {
|
|
21319
|
-
return
|
|
22353
|
+
return path23.join(resolveHermesProfileDir(profileName), "memories");
|
|
21320
22354
|
}
|
|
21321
22355
|
async function readMemoryStore(profileName, target, limits) {
|
|
21322
22356
|
const filePath = memoryFilePath(profileName, target);
|
|
21323
22357
|
const entries = await readMemoryEntries(filePath);
|
|
21324
|
-
const fileStat = await
|
|
21325
|
-
if (
|
|
22358
|
+
const fileStat = await stat16(filePath).catch((error) => {
|
|
22359
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21326
22360
|
return null;
|
|
21327
22361
|
}
|
|
21328
22362
|
throw error;
|
|
@@ -21350,8 +22384,8 @@ async function readMemoryStore(profileName, target, limits) {
|
|
|
21350
22384
|
};
|
|
21351
22385
|
}
|
|
21352
22386
|
async function readMemoryEntries(filePath) {
|
|
21353
|
-
const raw = await
|
|
21354
|
-
if (
|
|
22387
|
+
const raw = await readFile16(filePath, "utf8").catch((error) => {
|
|
22388
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21355
22389
|
return "";
|
|
21356
22390
|
}
|
|
21357
22391
|
throw error;
|
|
@@ -21377,7 +22411,7 @@ async function writeMemoryEntries(profileName, target, entries) {
|
|
|
21377
22411
|
);
|
|
21378
22412
|
}
|
|
21379
22413
|
function memoryFilePath(profileName, target) {
|
|
21380
|
-
return
|
|
22414
|
+
return path23.join(
|
|
21381
22415
|
resolveMemoryDir(profileName),
|
|
21382
22416
|
target === "user" ? "USER.md" : "MEMORY.md"
|
|
21383
22417
|
);
|
|
@@ -21410,7 +22444,7 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
|
|
|
21410
22444
|
}
|
|
21411
22445
|
return Promise.all(
|
|
21412
22446
|
[...descriptors.values()].map(async (descriptor) => {
|
|
21413
|
-
const
|
|
22447
|
+
const installed2 = await isUserMemoryProviderInstalled(
|
|
21414
22448
|
profileName,
|
|
21415
22449
|
descriptor.id
|
|
21416
22450
|
);
|
|
@@ -21420,8 +22454,8 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
|
|
|
21420
22454
|
description: descriptor.description,
|
|
21421
22455
|
active: descriptor.id === activeProviderId,
|
|
21422
22456
|
configurable: true,
|
|
21423
|
-
configured:
|
|
21424
|
-
configurationIssue:
|
|
22457
|
+
configured: installed2,
|
|
22458
|
+
configurationIssue: installed2 ? null : "\u6CA1\u6709\u5728\u5F53\u524D HERMES_HOME/plugins/ \u4E0B\u53D1\u73B0\u8FD9\u4E2A\u81EA\u5B9A\u4E49 memory provider\u3002",
|
|
21425
22459
|
providerConfigPath: customProviderConfigPath(profileName, descriptor.id),
|
|
21426
22460
|
settings: await readCustomProviderSettings(profileName, descriptor.id)
|
|
21427
22461
|
};
|
|
@@ -21437,7 +22471,7 @@ async function readCustomProviderSetupSummary(profileName) {
|
|
|
21437
22471
|
configurable: true,
|
|
21438
22472
|
configured: true,
|
|
21439
22473
|
configurationIssue: null,
|
|
21440
|
-
providerConfigPath:
|
|
22474
|
+
providerConfigPath: path23.join(
|
|
21441
22475
|
resolveHermesProfileDir(profileName),
|
|
21442
22476
|
"<provider>.json"
|
|
21443
22477
|
),
|
|
@@ -21766,8 +22800,8 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21766
22800
|
memoryProviderConfigPath(profileName, provider) ?? ""
|
|
21767
22801
|
);
|
|
21768
22802
|
const env = await readHermesMemoryEnv(profileName);
|
|
21769
|
-
const banks =
|
|
21770
|
-
const hermesBank =
|
|
22803
|
+
const banks = toRecord17(config.banks);
|
|
22804
|
+
const hermesBank = toRecord17(banks.hermes);
|
|
21771
22805
|
const mode = normalizeHindsightMode(config.mode);
|
|
21772
22806
|
return [
|
|
21773
22807
|
selectSetting("mode", "\u8FDE\u63A5\u6A21\u5F0F", mode, [
|
|
@@ -21831,7 +22865,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21831
22865
|
stringSetting(
|
|
21832
22866
|
"dbPath",
|
|
21833
22867
|
"SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
|
|
21834
|
-
config.db_path ??
|
|
22868
|
+
config.db_path ?? path23.join(resolveHermesProfileDir(profileName), "memory_store.db")
|
|
21835
22869
|
),
|
|
21836
22870
|
booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
|
|
21837
22871
|
numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
|
|
@@ -21867,7 +22901,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21867
22901
|
stringSetting(
|
|
21868
22902
|
"workingDirectory",
|
|
21869
22903
|
"\u5DE5\u4F5C\u76EE\u5F55",
|
|
21870
|
-
|
|
22904
|
+
path23.join(resolveHermesProfileDir(profileName), "byterover"),
|
|
21871
22905
|
false
|
|
21872
22906
|
)
|
|
21873
22907
|
];
|
|
@@ -21876,16 +22910,16 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21876
22910
|
}
|
|
21877
22911
|
function memoryProviderConfigPath(profileName, provider) {
|
|
21878
22912
|
if (provider === "honcho") {
|
|
21879
|
-
return
|
|
22913
|
+
return path23.join(resolveHermesProfileDir(profileName), "honcho.json");
|
|
21880
22914
|
}
|
|
21881
22915
|
if (provider === "mem0") {
|
|
21882
|
-
return
|
|
22916
|
+
return path23.join(resolveHermesProfileDir(profileName), "mem0.json");
|
|
21883
22917
|
}
|
|
21884
22918
|
if (provider === "supermemory") {
|
|
21885
|
-
return
|
|
22919
|
+
return path23.join(resolveHermesProfileDir(profileName), "supermemory.json");
|
|
21886
22920
|
}
|
|
21887
22921
|
if (provider === "hindsight") {
|
|
21888
|
-
return
|
|
22922
|
+
return path23.join(
|
|
21889
22923
|
resolveHermesProfileDir(profileName),
|
|
21890
22924
|
"hindsight",
|
|
21891
22925
|
"config.json"
|
|
@@ -21894,21 +22928,21 @@ function memoryProviderConfigPath(profileName, provider) {
|
|
|
21894
22928
|
return null;
|
|
21895
22929
|
}
|
|
21896
22930
|
function customProviderConfigPath(profileName, provider) {
|
|
21897
|
-
return
|
|
22931
|
+
return path23.join(
|
|
21898
22932
|
resolveHermesProfileDir(profileName),
|
|
21899
22933
|
`${normalizeCustomProviderId(provider)}.json`
|
|
21900
22934
|
);
|
|
21901
22935
|
}
|
|
21902
22936
|
function customProviderRegistryPath(profileName) {
|
|
21903
|
-
return
|
|
22937
|
+
return path23.join(
|
|
21904
22938
|
resolveHermesProfileDir(profileName),
|
|
21905
22939
|
CUSTOM_PROVIDER_REGISTRY_FILE
|
|
21906
22940
|
);
|
|
21907
22941
|
}
|
|
21908
22942
|
async function readCustomProviderRegistry(profileName) {
|
|
21909
|
-
const raw = await
|
|
22943
|
+
const raw = await readFile16(customProviderRegistryPath(profileName), "utf8").catch(
|
|
21910
22944
|
(error) => {
|
|
21911
|
-
if (
|
|
22945
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21912
22946
|
return "";
|
|
21913
22947
|
}
|
|
21914
22948
|
throw error;
|
|
@@ -21919,13 +22953,13 @@ async function readCustomProviderRegistry(profileName) {
|
|
|
21919
22953
|
}
|
|
21920
22954
|
try {
|
|
21921
22955
|
const parsed = JSON.parse(raw);
|
|
21922
|
-
const providers = Array.isArray(parsed) ? parsed : Array.isArray(
|
|
22956
|
+
const providers = Array.isArray(parsed) ? parsed : Array.isArray(toRecord17(parsed).providers) ? toRecord17(parsed).providers : [];
|
|
21923
22957
|
return providers.map((item) => {
|
|
21924
22958
|
if (typeof item === "string") {
|
|
21925
22959
|
const id2 = normalizeCustomProviderId(item);
|
|
21926
22960
|
return { id: id2, label: id2, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" };
|
|
21927
22961
|
}
|
|
21928
|
-
const record =
|
|
22962
|
+
const record = toRecord17(item);
|
|
21929
22963
|
const id = normalizeCustomProviderId(readString17(record.id) ?? "");
|
|
21930
22964
|
return {
|
|
21931
22965
|
id,
|
|
@@ -21952,10 +22986,10 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
|
|
|
21952
22986
|
);
|
|
21953
22987
|
}
|
|
21954
22988
|
async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
21955
|
-
const pluginsDir =
|
|
22989
|
+
const pluginsDir = path23.join(resolveHermesProfileDir(profileName), "plugins");
|
|
21956
22990
|
const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
|
|
21957
22991
|
(error) => {
|
|
21958
|
-
if (
|
|
22992
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21959
22993
|
return [];
|
|
21960
22994
|
}
|
|
21961
22995
|
throw error;
|
|
@@ -21972,7 +23006,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
21972
23006
|
} catch {
|
|
21973
23007
|
continue;
|
|
21974
23008
|
}
|
|
21975
|
-
const providerDir =
|
|
23009
|
+
const providerDir = path23.join(pluginsDir, entry.name);
|
|
21976
23010
|
if (!await isMemoryProviderPluginDir(providerDir)) {
|
|
21977
23011
|
continue;
|
|
21978
23012
|
}
|
|
@@ -21986,7 +23020,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
21986
23020
|
return descriptors;
|
|
21987
23021
|
}
|
|
21988
23022
|
async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
21989
|
-
const providerDir =
|
|
23023
|
+
const providerDir = path23.join(
|
|
21990
23024
|
resolveHermesProfileDir(profileName),
|
|
21991
23025
|
"plugins",
|
|
21992
23026
|
normalizeCustomProviderId(provider)
|
|
@@ -21994,9 +23028,9 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
|
21994
23028
|
return isMemoryProviderPluginDir(providerDir);
|
|
21995
23029
|
}
|
|
21996
23030
|
async function isMemoryProviderPluginDir(providerDir) {
|
|
21997
|
-
const source = await
|
|
23031
|
+
const source = await readFile16(path23.join(providerDir, "__init__.py"), "utf8").catch(
|
|
21998
23032
|
(error) => {
|
|
21999
|
-
if (
|
|
23033
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22000
23034
|
return "";
|
|
22001
23035
|
}
|
|
22002
23036
|
throw error;
|
|
@@ -22006,22 +23040,22 @@ async function isMemoryProviderPluginDir(providerDir) {
|
|
|
22006
23040
|
return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
|
|
22007
23041
|
}
|
|
22008
23042
|
async function readPluginMetadata(providerDir) {
|
|
22009
|
-
const raw = await
|
|
23043
|
+
const raw = await readFile16(path23.join(providerDir, "plugin.yaml"), "utf8").catch(
|
|
22010
23044
|
(error) => {
|
|
22011
|
-
if (
|
|
23045
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22012
23046
|
return "";
|
|
22013
23047
|
}
|
|
22014
23048
|
throw error;
|
|
22015
23049
|
}
|
|
22016
23050
|
);
|
|
22017
|
-
return raw ?
|
|
23051
|
+
return raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
22018
23052
|
}
|
|
22019
23053
|
async function resolveByteRoverCli() {
|
|
22020
23054
|
const candidates = [
|
|
22021
|
-
...(process.env.PATH ?? "").split(
|
|
22022
|
-
|
|
23055
|
+
...(process.env.PATH ?? "").split(path23.delimiter).filter(Boolean).map((dir) => path23.join(dir, "brv")),
|
|
23056
|
+
path23.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
|
|
22023
23057
|
"/usr/local/bin/brv",
|
|
22024
|
-
|
|
23058
|
+
path23.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
|
|
22025
23059
|
].filter(Boolean);
|
|
22026
23060
|
for (const candidate of candidates) {
|
|
22027
23061
|
const found = await access3(candidate).then(() => true).catch(() => false);
|
|
@@ -22032,32 +23066,32 @@ async function resolveByteRoverCli() {
|
|
|
22032
23066
|
return null;
|
|
22033
23067
|
}
|
|
22034
23068
|
async function readHolographicProviderConfig(profileName) {
|
|
22035
|
-
const raw = await
|
|
23069
|
+
const raw = await readFile16(resolveHermesConfigPath(profileName), "utf8").catch(
|
|
22036
23070
|
(error) => {
|
|
22037
|
-
if (
|
|
23071
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22038
23072
|
return "";
|
|
22039
23073
|
}
|
|
22040
23074
|
throw error;
|
|
22041
23075
|
}
|
|
22042
23076
|
);
|
|
22043
|
-
const config = raw ?
|
|
22044
|
-
const plugins =
|
|
22045
|
-
return
|
|
23077
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23078
|
+
const plugins = toRecord17(config.plugins);
|
|
23079
|
+
return toRecord17(plugins["hermes-memory-store"]);
|
|
22046
23080
|
}
|
|
22047
23081
|
async function patchHolographicProviderConfig(profileName, patch) {
|
|
22048
23082
|
const configPath = resolveHermesConfigPath(profileName);
|
|
22049
|
-
const existingRaw = await
|
|
23083
|
+
const existingRaw = await readFile16(configPath, "utf8").catch(
|
|
22050
23084
|
(error) => {
|
|
22051
|
-
if (
|
|
23085
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22052
23086
|
return null;
|
|
22053
23087
|
}
|
|
22054
23088
|
throw error;
|
|
22055
23089
|
}
|
|
22056
23090
|
);
|
|
22057
|
-
const document = existingRaw ?
|
|
22058
|
-
const config =
|
|
22059
|
-
const plugins =
|
|
22060
|
-
const memoryStore =
|
|
23091
|
+
const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
|
|
23092
|
+
const config = toRecord17(document.toJSON());
|
|
23093
|
+
const plugins = toRecord17(config.plugins);
|
|
23094
|
+
const memoryStore = toRecord17(plugins["hermes-memory-store"]);
|
|
22061
23095
|
for (const [key, value] of Object.entries(patch)) {
|
|
22062
23096
|
if (value !== void 0) {
|
|
22063
23097
|
memoryStore[key] = value;
|
|
@@ -22082,9 +23116,9 @@ async function patchHermesMemoryEnv(profileName, patch) {
|
|
|
22082
23116
|
if (entries.length === 0) {
|
|
22083
23117
|
return;
|
|
22084
23118
|
}
|
|
22085
|
-
const envPath =
|
|
22086
|
-
const existingRaw = await
|
|
22087
|
-
if (
|
|
23119
|
+
const envPath = path23.join(resolveHermesProfileDir(profileName), ".env");
|
|
23120
|
+
const existingRaw = await readFile16(envPath, "utf8").catch((error) => {
|
|
23121
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22088
23122
|
return "";
|
|
22089
23123
|
}
|
|
22090
23124
|
throw error;
|
|
@@ -22205,7 +23239,7 @@ function joinHindsightUrl(baseUrl, pathName) {
|
|
|
22205
23239
|
}
|
|
22206
23240
|
function parseJsonObject2(text) {
|
|
22207
23241
|
try {
|
|
22208
|
-
return
|
|
23242
|
+
return toRecord17(JSON.parse(text));
|
|
22209
23243
|
} catch {
|
|
22210
23244
|
return {};
|
|
22211
23245
|
}
|
|
@@ -22235,17 +23269,17 @@ function summarizeHindsightProbe(pathName, json) {
|
|
|
22235
23269
|
return bankId ? `bank ${bankId} reachable` : "bank config reachable";
|
|
22236
23270
|
}
|
|
22237
23271
|
async function readActiveMemoryProvider(profileName) {
|
|
22238
|
-
const raw = await
|
|
23272
|
+
const raw = await readFile16(
|
|
22239
23273
|
resolveHermesConfigPath(profileName),
|
|
22240
23274
|
"utf8"
|
|
22241
23275
|
).catch((error) => {
|
|
22242
|
-
if (
|
|
23276
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22243
23277
|
return "";
|
|
22244
23278
|
}
|
|
22245
23279
|
throw error;
|
|
22246
23280
|
});
|
|
22247
|
-
const config = raw ?
|
|
22248
|
-
const memory =
|
|
23281
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23282
|
+
const memory = toRecord17(config.memory);
|
|
22249
23283
|
const provider = readString17(memory.provider);
|
|
22250
23284
|
if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
|
|
22251
23285
|
return null;
|
|
@@ -22253,7 +23287,7 @@ async function readActiveMemoryProvider(profileName) {
|
|
|
22253
23287
|
return provider;
|
|
22254
23288
|
}
|
|
22255
23289
|
async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
22256
|
-
const configPath =
|
|
23290
|
+
const configPath = path23.join(
|
|
22257
23291
|
resolveHermesProfileDir(profileName),
|
|
22258
23292
|
relativePath
|
|
22259
23293
|
);
|
|
@@ -22271,18 +23305,18 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
|
22271
23305
|
);
|
|
22272
23306
|
}
|
|
22273
23307
|
async function readJsonObject(filePath) {
|
|
22274
|
-
const raw = await
|
|
22275
|
-
if (
|
|
23308
|
+
const raw = await readFile16(filePath, "utf8").catch((error) => {
|
|
23309
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22276
23310
|
return "{}";
|
|
22277
23311
|
}
|
|
22278
23312
|
throw error;
|
|
22279
23313
|
});
|
|
22280
23314
|
try {
|
|
22281
|
-
return
|
|
23315
|
+
return toRecord17(JSON.parse(raw || "{}"));
|
|
22282
23316
|
} catch {
|
|
22283
23317
|
throw new HermesMemoryError(
|
|
22284
23318
|
"memory_provider_config_invalid",
|
|
22285
|
-
`${
|
|
23319
|
+
`${path23.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
|
|
22286
23320
|
);
|
|
22287
23321
|
}
|
|
22288
23322
|
}
|
|
@@ -22332,17 +23366,17 @@ function selectSetting(key, label, value, options, editable = true) {
|
|
|
22332
23366
|
return { key, label, value: stringValue, editable, kind: "select", options };
|
|
22333
23367
|
}
|
|
22334
23368
|
async function readMemoryLimits(profileName) {
|
|
22335
|
-
const raw = await
|
|
23369
|
+
const raw = await readFile16(
|
|
22336
23370
|
resolveHermesConfigPath(profileName),
|
|
22337
23371
|
"utf8"
|
|
22338
23372
|
).catch((error) => {
|
|
22339
|
-
if (
|
|
23373
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22340
23374
|
return "";
|
|
22341
23375
|
}
|
|
22342
23376
|
throw error;
|
|
22343
23377
|
});
|
|
22344
|
-
const config = raw ?
|
|
22345
|
-
const memory =
|
|
23378
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23379
|
+
const memory = toRecord17(config.memory);
|
|
22346
23380
|
return {
|
|
22347
23381
|
memory: readPositiveInteger3(memory.memory_char_limit) ?? DEFAULT_MEMORY_LIMIT,
|
|
22348
23382
|
user: readPositiveInteger3(memory.user_char_limit) ?? DEFAULT_USER_LIMIT
|
|
@@ -22398,7 +23432,7 @@ function hashString(value) {
|
|
|
22398
23432
|
}
|
|
22399
23433
|
return hash.toString(16);
|
|
22400
23434
|
}
|
|
22401
|
-
function
|
|
23435
|
+
function toRecord17(value) {
|
|
22402
23436
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
22403
23437
|
}
|
|
22404
23438
|
function readString17(value) {
|
|
@@ -22429,7 +23463,7 @@ function formatEnvValue3(value) {
|
|
|
22429
23463
|
function escapeRegExp4(value) {
|
|
22430
23464
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
22431
23465
|
}
|
|
22432
|
-
function
|
|
23466
|
+
function isNodeError18(error, code) {
|
|
22433
23467
|
return error instanceof Error && "code" in error && error.code === code;
|
|
22434
23468
|
}
|
|
22435
23469
|
|
|
@@ -22870,9 +23904,9 @@ function toMemoryHttpError(error) {
|
|
|
22870
23904
|
}
|
|
22871
23905
|
|
|
22872
23906
|
// src/hermes/skills.ts
|
|
22873
|
-
import { readFile as
|
|
22874
|
-
import
|
|
22875
|
-
import
|
|
23907
|
+
import { readFile as readFile17, readdir as readdir11 } from "fs/promises";
|
|
23908
|
+
import path24 from "path";
|
|
23909
|
+
import YAML6 from "yaml";
|
|
22876
23910
|
var HermesSkillNotFoundError = class extends Error {
|
|
22877
23911
|
constructor(skillName) {
|
|
22878
23912
|
super(`skill "${skillName}" does not exist`);
|
|
@@ -22885,7 +23919,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
|
|
|
22885
23919
|
async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
|
|
22886
23920
|
const profile = await readExistingProfile(profileName, paths);
|
|
22887
23921
|
const profileDir = resolveHermesProfileDir(profile.name);
|
|
22888
|
-
const skillsRoot =
|
|
23922
|
+
const skillsRoot = path24.join(profileDir, "skills");
|
|
22889
23923
|
const [skillFiles, disabled, provenance] = await Promise.all([
|
|
22890
23924
|
findSkillFiles(skillsRoot),
|
|
22891
23925
|
readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
|
|
@@ -22921,8 +23955,8 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
|
|
|
22921
23955
|
throw new HermesSkillNotFoundError(skillName);
|
|
22922
23956
|
}
|
|
22923
23957
|
const configPath = resolveHermesConfigPath(current.profile.name);
|
|
22924
|
-
const { document, config, existingRaw } = await
|
|
22925
|
-
const skillsConfig =
|
|
23958
|
+
const { document, config, existingRaw } = await readHermesConfigDocument3(configPath);
|
|
23959
|
+
const skillsConfig = ensureRecord4(config, "skills");
|
|
22926
23960
|
const disabled = new Set(readStringList3(skillsConfig.disabled));
|
|
22927
23961
|
if (enabled) {
|
|
22928
23962
|
disabled.delete(target.name);
|
|
@@ -22932,7 +23966,7 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
|
|
|
22932
23966
|
skillsConfig.disabled = [...disabled].sort(
|
|
22933
23967
|
(left, right) => left.localeCompare(right)
|
|
22934
23968
|
);
|
|
22935
|
-
const backupPath = await
|
|
23969
|
+
const backupPath = await writeHermesConfigDocument3({
|
|
22936
23970
|
configPath,
|
|
22937
23971
|
document,
|
|
22938
23972
|
config,
|
|
@@ -22964,7 +23998,7 @@ async function findSkillFiles(root) {
|
|
|
22964
23998
|
async function collectSkillFiles(directory, results) {
|
|
22965
23999
|
const entries = await readdir11(directory, { withFileTypes: true }).catch(
|
|
22966
24000
|
(error) => {
|
|
22967
|
-
if (
|
|
24001
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
22968
24002
|
return [];
|
|
22969
24003
|
}
|
|
22970
24004
|
throw error;
|
|
@@ -22976,7 +24010,7 @@ async function collectSkillFiles(directory, results) {
|
|
|
22976
24010
|
if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
|
|
22977
24011
|
continue;
|
|
22978
24012
|
}
|
|
22979
|
-
const entryPath =
|
|
24013
|
+
const entryPath = path24.join(directory, entry.name);
|
|
22980
24014
|
if (entry.isDirectory()) {
|
|
22981
24015
|
await collectSkillFiles(entryPath, results);
|
|
22982
24016
|
continue;
|
|
@@ -22987,9 +24021,9 @@ async function collectSkillFiles(directory, results) {
|
|
|
22987
24021
|
}
|
|
22988
24022
|
}
|
|
22989
24023
|
async function readSkillMetadata(input) {
|
|
22990
|
-
const raw = await
|
|
24024
|
+
const raw = await readFile17(input.skillFile, "utf8").catch(
|
|
22991
24025
|
(error) => {
|
|
22992
|
-
if (
|
|
24026
|
+
if (isNodeError19(error, "ENOENT") || isNodeError19(error, "EACCES")) {
|
|
22993
24027
|
return null;
|
|
22994
24028
|
}
|
|
22995
24029
|
throw error;
|
|
@@ -22998,10 +24032,10 @@ async function readSkillMetadata(input) {
|
|
|
22998
24032
|
if (raw === null) {
|
|
22999
24033
|
return null;
|
|
23000
24034
|
}
|
|
23001
|
-
const skillDir =
|
|
24035
|
+
const skillDir = path24.dirname(input.skillFile);
|
|
23002
24036
|
const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
|
|
23003
24037
|
const name = normalizeSkillName(
|
|
23004
|
-
readString18(frontmatter.name) ??
|
|
24038
|
+
readString18(frontmatter.name) ?? path24.basename(skillDir)
|
|
23005
24039
|
);
|
|
23006
24040
|
if (!name) {
|
|
23007
24041
|
return null;
|
|
@@ -23020,7 +24054,7 @@ async function readSkillMetadata(input) {
|
|
|
23020
24054
|
enabled: !input.disabled.has(name),
|
|
23021
24055
|
source: provenance.source,
|
|
23022
24056
|
trust: provenance.trust,
|
|
23023
|
-
relativePath:
|
|
24057
|
+
relativePath: path24.relative(input.skillsRoot, skillDir)
|
|
23024
24058
|
};
|
|
23025
24059
|
}
|
|
23026
24060
|
function parseSkillDocument(raw) {
|
|
@@ -23033,7 +24067,7 @@ function parseSkillDocument(raw) {
|
|
|
23033
24067
|
}
|
|
23034
24068
|
try {
|
|
23035
24069
|
return {
|
|
23036
|
-
frontmatter:
|
|
24070
|
+
frontmatter: toRecord18(YAML6.parse(match[1] ?? "")),
|
|
23037
24071
|
body: content.slice(match[0].length)
|
|
23038
24072
|
};
|
|
23039
24073
|
} catch {
|
|
@@ -23041,8 +24075,8 @@ function parseSkillDocument(raw) {
|
|
|
23041
24075
|
}
|
|
23042
24076
|
}
|
|
23043
24077
|
function categoryFromPath(skillsRoot, skillFile) {
|
|
23044
|
-
const relative =
|
|
23045
|
-
const parts = relative.split(
|
|
24078
|
+
const relative = path24.relative(skillsRoot, skillFile);
|
|
24079
|
+
const parts = relative.split(path24.sep).filter(Boolean);
|
|
23046
24080
|
return parts.length >= 3 ? parts[0] : null;
|
|
23047
24081
|
}
|
|
23048
24082
|
function firstBodyDescription(body) {
|
|
@@ -23065,8 +24099,8 @@ function normalizeDescription(value) {
|
|
|
23065
24099
|
return `${description.slice(0, MAX_DESCRIPTION_LENGTH - 3)}...`;
|
|
23066
24100
|
}
|
|
23067
24101
|
async function readDisabledSkillNames(configPath) {
|
|
23068
|
-
const raw = await
|
|
23069
|
-
if (
|
|
24102
|
+
const raw = await readFile17(configPath, "utf8").catch((error) => {
|
|
24103
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23070
24104
|
return "";
|
|
23071
24105
|
}
|
|
23072
24106
|
throw error;
|
|
@@ -23074,8 +24108,8 @@ async function readDisabledSkillNames(configPath) {
|
|
|
23074
24108
|
if (!raw.trim()) {
|
|
23075
24109
|
return /* @__PURE__ */ new Set();
|
|
23076
24110
|
}
|
|
23077
|
-
const config =
|
|
23078
|
-
const skills =
|
|
24111
|
+
const config = toRecord18(YAML6.parse(raw));
|
|
24112
|
+
const skills = toRecord18(config.skills);
|
|
23079
24113
|
return new Set(readStringList3(skills.disabled));
|
|
23080
24114
|
}
|
|
23081
24115
|
async function readSkillProvenance(root) {
|
|
@@ -23089,9 +24123,9 @@ async function readSkillProvenance(root) {
|
|
|
23089
24123
|
return provenance;
|
|
23090
24124
|
}
|
|
23091
24125
|
async function readBundledSkillNames(root) {
|
|
23092
|
-
const raw = await
|
|
24126
|
+
const raw = await readFile17(path24.join(root, ".bundled_manifest"), "utf8").catch(
|
|
23093
24127
|
(error) => {
|
|
23094
|
-
if (
|
|
24128
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23095
24129
|
return "";
|
|
23096
24130
|
}
|
|
23097
24131
|
throw error;
|
|
@@ -23112,9 +24146,9 @@ async function readBundledSkillNames(root) {
|
|
|
23112
24146
|
return names;
|
|
23113
24147
|
}
|
|
23114
24148
|
async function readHubInstalledSkills(root) {
|
|
23115
|
-
const raw = await
|
|
24149
|
+
const raw = await readFile17(path24.join(root, ".hub", "lock.json"), "utf8").catch(
|
|
23116
24150
|
(error) => {
|
|
23117
|
-
if (
|
|
24151
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23118
24152
|
return "";
|
|
23119
24153
|
}
|
|
23120
24154
|
throw error;
|
|
@@ -23125,14 +24159,14 @@ async function readHubInstalledSkills(root) {
|
|
|
23125
24159
|
}
|
|
23126
24160
|
let lock;
|
|
23127
24161
|
try {
|
|
23128
|
-
lock =
|
|
24162
|
+
lock = toRecord18(JSON.parse(raw));
|
|
23129
24163
|
} catch {
|
|
23130
24164
|
return /* @__PURE__ */ new Map();
|
|
23131
24165
|
}
|
|
23132
|
-
const
|
|
24166
|
+
const installed2 = toRecord18(lock.installed);
|
|
23133
24167
|
const result = /* @__PURE__ */ new Map();
|
|
23134
|
-
for (const [name, rawEntry] of Object.entries(
|
|
23135
|
-
const entry =
|
|
24168
|
+
for (const [name, rawEntry] of Object.entries(installed2)) {
|
|
24169
|
+
const entry = toRecord18(rawEntry);
|
|
23136
24170
|
result.set(normalizeSkillName(name), {
|
|
23137
24171
|
source: readString18(entry.source) ?? "hub",
|
|
23138
24172
|
trust: readString18(entry.trust_level) ?? null
|
|
@@ -23183,23 +24217,23 @@ function compareCategoryNames(left, right) {
|
|
|
23183
24217
|
}
|
|
23184
24218
|
return left.localeCompare(right);
|
|
23185
24219
|
}
|
|
23186
|
-
async function
|
|
23187
|
-
const existingRaw = await
|
|
24220
|
+
async function readHermesConfigDocument3(configPath) {
|
|
24221
|
+
const existingRaw = await readFile17(configPath, "utf8").catch(
|
|
23188
24222
|
(error) => {
|
|
23189
|
-
if (
|
|
24223
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23190
24224
|
return null;
|
|
23191
24225
|
}
|
|
23192
24226
|
throw error;
|
|
23193
24227
|
}
|
|
23194
24228
|
);
|
|
23195
|
-
const document = existingRaw ?
|
|
24229
|
+
const document = existingRaw ? YAML6.parseDocument(existingRaw) : new YAML6.Document({});
|
|
23196
24230
|
return {
|
|
23197
24231
|
document,
|
|
23198
|
-
config:
|
|
24232
|
+
config: toRecord18(document.toJSON()),
|
|
23199
24233
|
existingRaw
|
|
23200
24234
|
};
|
|
23201
24235
|
}
|
|
23202
|
-
async function
|
|
24236
|
+
async function writeHermesConfigDocument3(input) {
|
|
23203
24237
|
const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
|
|
23204
24238
|
if (backupPath) {
|
|
23205
24239
|
await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
|
|
@@ -23222,18 +24256,18 @@ function readStringList3(value) {
|
|
|
23222
24256
|
function readString18(value) {
|
|
23223
24257
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
23224
24258
|
}
|
|
23225
|
-
function
|
|
24259
|
+
function toRecord18(value) {
|
|
23226
24260
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
23227
24261
|
}
|
|
23228
|
-
function
|
|
23229
|
-
const current =
|
|
24262
|
+
function ensureRecord4(target, key) {
|
|
24263
|
+
const current = toRecord18(target[key]);
|
|
23230
24264
|
if (current === target[key]) {
|
|
23231
24265
|
return current;
|
|
23232
24266
|
}
|
|
23233
24267
|
target[key] = current;
|
|
23234
24268
|
return current;
|
|
23235
24269
|
}
|
|
23236
|
-
function
|
|
24270
|
+
function isNodeError19(error, code) {
|
|
23237
24271
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
23238
24272
|
}
|
|
23239
24273
|
|
|
@@ -23716,8 +24750,8 @@ function readModelList(payload) {
|
|
|
23716
24750
|
// src/hermes/updates.ts
|
|
23717
24751
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
23718
24752
|
import { spawn as spawn3 } from "child_process";
|
|
23719
|
-
import { mkdir as mkdir12, readFile as
|
|
23720
|
-
import
|
|
24753
|
+
import { mkdir as mkdir12, readFile as readFile18, rm as rm7 } from "fs/promises";
|
|
24754
|
+
import path25 from "path";
|
|
23721
24755
|
var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
|
|
23722
24756
|
var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
23723
24757
|
var RELEASE_FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -23950,7 +24984,7 @@ async function readRemoteRelease(options, now) {
|
|
|
23950
24984
|
}
|
|
23951
24985
|
}
|
|
23952
24986
|
function normalizeServerReleaseSnapshot(payload) {
|
|
23953
|
-
const snapshot =
|
|
24987
|
+
const snapshot = toRecord19(payload);
|
|
23954
24988
|
const remote = toNullableRecord(snapshot.remote);
|
|
23955
24989
|
return {
|
|
23956
24990
|
remote: remote ? normalizeServerRelease(remote) : null,
|
|
@@ -23986,7 +25020,7 @@ async function writeUpdateState(paths, state) {
|
|
|
23986
25020
|
await writeJsonFile(updateStatePath(paths), state);
|
|
23987
25021
|
}
|
|
23988
25022
|
async function readUpdateLogLines(paths) {
|
|
23989
|
-
const raw = await
|
|
25023
|
+
const raw = await readFile18(updateLogPath(paths), "utf8").catch(() => "");
|
|
23990
25024
|
if (!raw.trim()) {
|
|
23991
25025
|
return [];
|
|
23992
25026
|
}
|
|
@@ -23995,13 +25029,13 @@ async function readUpdateLogLines(paths) {
|
|
|
23995
25029
|
);
|
|
23996
25030
|
}
|
|
23997
25031
|
function releaseCachePath(paths) {
|
|
23998
|
-
return
|
|
25032
|
+
return path25.join(paths.indexesDir, "hermes-release-check.json");
|
|
23999
25033
|
}
|
|
24000
25034
|
function updateStatePath(paths) {
|
|
24001
|
-
return
|
|
25035
|
+
return path25.join(paths.runDir, "hermes-update-state.json");
|
|
24002
25036
|
}
|
|
24003
25037
|
function updateLogPath(paths) {
|
|
24004
|
-
return
|
|
25038
|
+
return path25.join(paths.logsDir, UPDATE_LOG_FILE);
|
|
24005
25039
|
}
|
|
24006
25040
|
async function clearUpdateLogFiles(paths) {
|
|
24007
25041
|
const primary = updateLogPath(paths);
|
|
@@ -24039,7 +25073,7 @@ function compareSemver2(left, right) {
|
|
|
24039
25073
|
}
|
|
24040
25074
|
return 0;
|
|
24041
25075
|
}
|
|
24042
|
-
function
|
|
25076
|
+
function toRecord19(value) {
|
|
24043
25077
|
return typeof value === "object" && value !== null ? value : {};
|
|
24044
25078
|
}
|
|
24045
25079
|
function toNullableRecord(value) {
|
|
@@ -24101,13 +25135,13 @@ function readString19(payload, key) {
|
|
|
24101
25135
|
// src/link/updates.ts
|
|
24102
25136
|
import { spawn as spawn5 } from "child_process";
|
|
24103
25137
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
24104
|
-
import { mkdir as mkdir15, readFile as
|
|
24105
|
-
import
|
|
25138
|
+
import { mkdir as mkdir15, readFile as readFile20, rm as rm10 } from "fs/promises";
|
|
25139
|
+
import path27 from "path";
|
|
24106
25140
|
|
|
24107
25141
|
// src/daemon/process.ts
|
|
24108
25142
|
import { spawn as spawn4 } from "child_process";
|
|
24109
|
-
import { mkdir as mkdir14, readFile as
|
|
24110
|
-
import
|
|
25143
|
+
import { mkdir as mkdir14, readFile as readFile19, rm as rm9, writeFile as writeFile4 } from "fs/promises";
|
|
25144
|
+
import path26 from "path";
|
|
24111
25145
|
|
|
24112
25146
|
// src/daemon/service.ts
|
|
24113
25147
|
import { createServer } from "http";
|
|
@@ -24116,6 +25150,121 @@ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promise
|
|
|
24116
25150
|
// src/relay/control-client.ts
|
|
24117
25151
|
import WebSocket from "ws";
|
|
24118
25152
|
|
|
25153
|
+
// src/relay/reconnect-state.ts
|
|
25154
|
+
var DEFAULT_STORM_WINDOW_MS = 5 * 6e4;
|
|
25155
|
+
var DEFAULT_STORM_DISCONNECT_LIMIT = 8;
|
|
25156
|
+
var DEFAULT_COOLDOWN_MS = 3 * 6e4;
|
|
25157
|
+
var DEFAULT_RELAY_RECONNECT_BASE_MS = 3e3;
|
|
25158
|
+
var DEFAULT_RELAY_RECONNECT_MAX_MS = 6e4;
|
|
25159
|
+
async function readRelayCooldownDelayMs(paths, now = /* @__PURE__ */ new Date()) {
|
|
25160
|
+
const state = await readLinkState(paths);
|
|
25161
|
+
const reconnect = normalizeRelayReconnectState(state.relayReconnect);
|
|
25162
|
+
const cooldownUntilMs = parseTimeMs(reconnect.cooldownUntil);
|
|
25163
|
+
if (!Number.isFinite(cooldownUntilMs)) {
|
|
25164
|
+
return 0;
|
|
25165
|
+
}
|
|
25166
|
+
return Math.max(0, cooldownUntilMs - now.getTime());
|
|
25167
|
+
}
|
|
25168
|
+
async function recordRelayDisconnect(paths, options = {}) {
|
|
25169
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
25170
|
+
const nowMs = now.getTime();
|
|
25171
|
+
const stormWindowMs = positiveInteger(options.stormWindowMs, DEFAULT_STORM_WINDOW_MS);
|
|
25172
|
+
const stormDisconnectLimit = positiveInteger(
|
|
25173
|
+
options.stormDisconnectLimit,
|
|
25174
|
+
DEFAULT_STORM_DISCONNECT_LIMIT
|
|
25175
|
+
);
|
|
25176
|
+
const cooldownMs = positiveInteger(options.cooldownMs, DEFAULT_COOLDOWN_MS);
|
|
25177
|
+
let result = {
|
|
25178
|
+
disconnectCount: 0,
|
|
25179
|
+
cooldownUntilMs: null
|
|
25180
|
+
};
|
|
25181
|
+
await updateRelayReconnectState(paths, (current) => {
|
|
25182
|
+
const recentDisconnects = [
|
|
25183
|
+
...current.recentDisconnects.filter((value) => {
|
|
25184
|
+
const timestamp = parseTimeMs(value);
|
|
25185
|
+
return Number.isFinite(timestamp) && nowMs - timestamp <= stormWindowMs;
|
|
25186
|
+
}),
|
|
25187
|
+
now.toISOString()
|
|
25188
|
+
];
|
|
25189
|
+
const enteredCooldown = recentDisconnects.length >= stormDisconnectLimit;
|
|
25190
|
+
const cooldownUntil = enteredCooldown ? new Date(nowMs + cooldownMs).toISOString() : current.cooldownUntil;
|
|
25191
|
+
const cooldownUntilMs = enteredCooldown ? nowMs + cooldownMs : null;
|
|
25192
|
+
result = {
|
|
25193
|
+
disconnectCount: recentDisconnects.length,
|
|
25194
|
+
cooldownUntilMs
|
|
25195
|
+
};
|
|
25196
|
+
return {
|
|
25197
|
+
recentDisconnects: enteredCooldown ? [] : recentDisconnects,
|
|
25198
|
+
cooldownUntil,
|
|
25199
|
+
lastFailureAt: now.toISOString(),
|
|
25200
|
+
lastFailureReason: normalizeReason(options.reason)
|
|
25201
|
+
};
|
|
25202
|
+
});
|
|
25203
|
+
return result;
|
|
25204
|
+
}
|
|
25205
|
+
async function clearRelayReconnectState(paths) {
|
|
25206
|
+
await updateRelayReconnectState(paths, (current) => ({
|
|
25207
|
+
...current,
|
|
25208
|
+
recentDisconnects: [],
|
|
25209
|
+
cooldownUntil: null
|
|
25210
|
+
}));
|
|
25211
|
+
}
|
|
25212
|
+
function computeRelayBackoffMs(attempt, options = {}) {
|
|
25213
|
+
const baseMs = positiveInteger(options.baseMs, DEFAULT_RELAY_RECONNECT_BASE_MS);
|
|
25214
|
+
const maxMs = positiveInteger(options.maxMs, DEFAULT_RELAY_RECONNECT_MAX_MS);
|
|
25215
|
+
const normalizedAttempt = Math.max(1, Math.floor(attempt));
|
|
25216
|
+
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, normalizedAttempt - 1));
|
|
25217
|
+
const random = options.random ?? Math.random;
|
|
25218
|
+
const ratio = 0.2 + clampRandom(random()) * 0.1;
|
|
25219
|
+
return exponential + Math.floor(exponential * ratio);
|
|
25220
|
+
}
|
|
25221
|
+
async function updateRelayReconnectState(paths, update) {
|
|
25222
|
+
const state = await readLinkState(paths);
|
|
25223
|
+
const next = {
|
|
25224
|
+
...state,
|
|
25225
|
+
relayReconnect: update(normalizeRelayReconnectState(state.relayReconnect))
|
|
25226
|
+
};
|
|
25227
|
+
await writeJsonFile(paths.stateFile, next);
|
|
25228
|
+
}
|
|
25229
|
+
async function readLinkState(paths) {
|
|
25230
|
+
const state = await readJsonFile(paths.stateFile);
|
|
25231
|
+
return state && typeof state === "object" ? state : {};
|
|
25232
|
+
}
|
|
25233
|
+
function normalizeRelayReconnectState(value) {
|
|
25234
|
+
const record = value && typeof value === "object" ? value : {};
|
|
25235
|
+
return {
|
|
25236
|
+
recentDisconnects: normalizeTimestamps(record.recentDisconnects),
|
|
25237
|
+
cooldownUntil: typeof record.cooldownUntil === "string" ? record.cooldownUntil : null,
|
|
25238
|
+
lastFailureAt: typeof record.lastFailureAt === "string" ? record.lastFailureAt : null,
|
|
25239
|
+
lastFailureReason: typeof record.lastFailureReason === "string" ? record.lastFailureReason : null
|
|
25240
|
+
};
|
|
25241
|
+
}
|
|
25242
|
+
function normalizeTimestamps(value) {
|
|
25243
|
+
if (!Array.isArray(value)) {
|
|
25244
|
+
return [];
|
|
25245
|
+
}
|
|
25246
|
+
return value.filter((item) => typeof item === "string" && Number.isFinite(parseTimeMs(item)));
|
|
25247
|
+
}
|
|
25248
|
+
function normalizeReason(value) {
|
|
25249
|
+
if (typeof value !== "string") {
|
|
25250
|
+
return null;
|
|
25251
|
+
}
|
|
25252
|
+
const trimmed = value.trim();
|
|
25253
|
+
return trimmed ? trimmed.slice(0, 240) : null;
|
|
25254
|
+
}
|
|
25255
|
+
function positiveInteger(value, fallback) {
|
|
25256
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
|
|
25257
|
+
}
|
|
25258
|
+
function parseTimeMs(value) {
|
|
25259
|
+
return typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
25260
|
+
}
|
|
25261
|
+
function clampRandom(value) {
|
|
25262
|
+
if (!Number.isFinite(value)) {
|
|
25263
|
+
return 0;
|
|
25264
|
+
}
|
|
25265
|
+
return Math.min(1, Math.max(0, value));
|
|
25266
|
+
}
|
|
25267
|
+
|
|
24119
25268
|
// src/relay/stream-policy.ts
|
|
24120
25269
|
var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
|
|
24121
25270
|
flushIntervalMs: 1e3,
|
|
@@ -24186,23 +25335,76 @@ function connectRelayControl(options) {
|
|
|
24186
25335
|
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
24187
25336
|
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
24188
25337
|
wsUrl.searchParams.set("link_id", options.linkId);
|
|
24189
|
-
const
|
|
24190
|
-
const
|
|
24191
|
-
const
|
|
25338
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
25339
|
+
const maxReconnectAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
|
|
25340
|
+
const backoffBaseMs = options.backoffBaseMs ?? DEFAULT_RELAY_RECONNECT_BASE_MS;
|
|
25341
|
+
const backoffMaxMs = options.backoffMaxMs ?? DEFAULT_RELAY_RECONNECT_MAX_MS;
|
|
24192
25342
|
let reconnectAttempts = 0;
|
|
24193
25343
|
let closedByUser = false;
|
|
24194
25344
|
let socket = null;
|
|
24195
25345
|
let retryTimer = null;
|
|
24196
25346
|
let abortControllers = /* @__PURE__ */ new Map();
|
|
24197
25347
|
let fatalRelayRejection = null;
|
|
25348
|
+
let relayRetryAfterMs = null;
|
|
24198
25349
|
let latestNetworkRoutes = null;
|
|
24199
25350
|
const streamBatchPolicy = {
|
|
24200
25351
|
current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
|
|
24201
25352
|
onUpdate: options.onStreamBatchPolicy
|
|
24202
25353
|
};
|
|
25354
|
+
const startConnect = () => {
|
|
25355
|
+
void waitForPersistedCooldown().then((delay3) => {
|
|
25356
|
+
if (closedByUser) {
|
|
25357
|
+
return;
|
|
25358
|
+
}
|
|
25359
|
+
if (delay3 > 0) {
|
|
25360
|
+
scheduleTimer(delay3, "cooldown", `Relay reconnect cooldown active for ${delay3}ms`);
|
|
25361
|
+
return;
|
|
25362
|
+
}
|
|
25363
|
+
connect();
|
|
25364
|
+
}).catch((error) => {
|
|
25365
|
+
if (closedByUser) {
|
|
25366
|
+
return;
|
|
25367
|
+
}
|
|
25368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25369
|
+
scheduleTimer(backoffBaseMs, "retrying", `Relay connect setup failed: ${message}`);
|
|
25370
|
+
});
|
|
25371
|
+
};
|
|
24203
25372
|
const connect = () => {
|
|
24204
25373
|
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
24205
25374
|
fatalRelayRejection = null;
|
|
25375
|
+
relayRetryAfterMs = null;
|
|
25376
|
+
let closeHandled = false;
|
|
25377
|
+
const handleConnectionClosed = (reason) => {
|
|
25378
|
+
if (closeHandled) {
|
|
25379
|
+
return;
|
|
25380
|
+
}
|
|
25381
|
+
closeHandled = true;
|
|
25382
|
+
abortAll(abortControllers);
|
|
25383
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
25384
|
+
if (fatalRelayRejection) {
|
|
25385
|
+
options.onStatus?.({
|
|
25386
|
+
state: "failed",
|
|
25387
|
+
attempt: reconnectAttempts,
|
|
25388
|
+
message: fatalRelayRejection
|
|
25389
|
+
});
|
|
25390
|
+
return;
|
|
25391
|
+
}
|
|
25392
|
+
if (closedByUser) {
|
|
25393
|
+
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
25394
|
+
return;
|
|
25395
|
+
}
|
|
25396
|
+
if (Number.isFinite(maxReconnectAttempts) && reconnectAttempts >= maxReconnectAttempts) {
|
|
25397
|
+
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
25398
|
+
return;
|
|
25399
|
+
}
|
|
25400
|
+
void scheduleReconnect(reason).catch((error) => {
|
|
25401
|
+
if (closedByUser) {
|
|
25402
|
+
return;
|
|
25403
|
+
}
|
|
25404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25405
|
+
scheduleTimer(backoffMaxMs, "retrying", `Relay reconnect scheduling failed: ${message}`);
|
|
25406
|
+
});
|
|
25407
|
+
};
|
|
24206
25408
|
socket = new WebSocket(wsUrl, {
|
|
24207
25409
|
headers: {
|
|
24208
25410
|
"x-hermes-link-version": LINK_VERSION
|
|
@@ -24210,6 +25412,7 @@ function connectRelayControl(options) {
|
|
|
24210
25412
|
});
|
|
24211
25413
|
socket.on("open", () => {
|
|
24212
25414
|
reconnectAttempts = 0;
|
|
25415
|
+
void clearRelayReconnectState(paths).catch(() => void 0);
|
|
24213
25416
|
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
24214
25417
|
const currentSocket = socket;
|
|
24215
25418
|
if (currentSocket && latestNetworkRoutes) {
|
|
@@ -24225,6 +25428,20 @@ function connectRelayControl(options) {
|
|
|
24225
25428
|
socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
24226
25429
|
});
|
|
24227
25430
|
});
|
|
25431
|
+
socket.on("unexpected-response", (request, response) => {
|
|
25432
|
+
const statusCode = response.statusCode ?? 0;
|
|
25433
|
+
fatalRelayRejection = resolveFatalRelayRejectionFromStatus(statusCode);
|
|
25434
|
+
relayRetryAfterMs = readRetryAfterMs(response);
|
|
25435
|
+
const message = fatalRelayRejection ?? `Relay returned HTTP ${statusCode || "unknown"}`;
|
|
25436
|
+
options.onStatus?.({
|
|
25437
|
+
state: "disconnected",
|
|
25438
|
+
attempt: reconnectAttempts,
|
|
25439
|
+
message
|
|
25440
|
+
});
|
|
25441
|
+
response.resume();
|
|
25442
|
+
handleConnectionClosed(message);
|
|
25443
|
+
request.destroy();
|
|
25444
|
+
});
|
|
24228
25445
|
socket.on("error", (error) => {
|
|
24229
25446
|
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
24230
25447
|
fatalRelayRejection = resolveFatalRelayRejection(message);
|
|
@@ -24235,32 +25452,40 @@ function connectRelayControl(options) {
|
|
|
24235
25452
|
});
|
|
24236
25453
|
});
|
|
24237
25454
|
socket.on("close", () => {
|
|
24238
|
-
|
|
24239
|
-
abortControllers = /* @__PURE__ */ new Map();
|
|
24240
|
-
if (fatalRelayRejection) {
|
|
24241
|
-
options.onStatus?.({
|
|
24242
|
-
state: "failed",
|
|
24243
|
-
attempt: reconnectAttempts,
|
|
24244
|
-
message: fatalRelayRejection
|
|
24245
|
-
});
|
|
24246
|
-
return;
|
|
24247
|
-
}
|
|
24248
|
-
if (closedByUser) {
|
|
24249
|
-
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
24250
|
-
return;
|
|
24251
|
-
}
|
|
24252
|
-
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
24253
|
-
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
24254
|
-
return;
|
|
24255
|
-
}
|
|
24256
|
-
reconnectAttempts += 1;
|
|
24257
|
-
const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
24258
|
-
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
|
|
24259
|
-
retryTimer = setTimeout(connect, delay3);
|
|
24260
|
-
retryTimer.unref?.();
|
|
25455
|
+
handleConnectionClosed();
|
|
24261
25456
|
});
|
|
24262
25457
|
};
|
|
24263
|
-
|
|
25458
|
+
startConnect();
|
|
25459
|
+
async function scheduleReconnect(reason) {
|
|
25460
|
+
const recorded = await recordRelayDisconnect(paths, { reason }).catch(() => ({
|
|
25461
|
+
disconnectCount: 0,
|
|
25462
|
+
cooldownUntilMs: null
|
|
25463
|
+
}));
|
|
25464
|
+
if (closedByUser) {
|
|
25465
|
+
return;
|
|
25466
|
+
}
|
|
25467
|
+
if (recorded.cooldownUntilMs !== null) {
|
|
25468
|
+
reconnectAttempts = 0;
|
|
25469
|
+
const delay4 = Math.max(0, recorded.cooldownUntilMs - Date.now());
|
|
25470
|
+
scheduleTimer(delay4, "cooldown", `Relay reconnect storm guard active for ${delay4}ms`);
|
|
25471
|
+
return;
|
|
25472
|
+
}
|
|
25473
|
+
reconnectAttempts += 1;
|
|
25474
|
+
const backoffMs = computeRelayBackoffMs(reconnectAttempts, {
|
|
25475
|
+
baseMs: backoffBaseMs,
|
|
25476
|
+
maxMs: backoffMaxMs
|
|
25477
|
+
});
|
|
25478
|
+
const delay3 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
|
|
25479
|
+
scheduleTimer(delay3, "retrying", `Retrying in ${delay3}ms`);
|
|
25480
|
+
}
|
|
25481
|
+
async function waitForPersistedCooldown() {
|
|
25482
|
+
return await readRelayCooldownDelayMs(paths).catch(() => 0);
|
|
25483
|
+
}
|
|
25484
|
+
function scheduleTimer(delay3, state, message) {
|
|
25485
|
+
options.onStatus?.({ state, attempt: reconnectAttempts, message });
|
|
25486
|
+
retryTimer = setTimeout(connect, delay3);
|
|
25487
|
+
retryTimer.unref?.();
|
|
25488
|
+
}
|
|
24264
25489
|
return {
|
|
24265
25490
|
publishNetworkRoutes(routes) {
|
|
24266
25491
|
latestNetworkRoutes = routes;
|
|
@@ -24297,7 +25522,12 @@ function sendNetworkRoutes(socket, linkId, routes) {
|
|
|
24297
25522
|
}));
|
|
24298
25523
|
}
|
|
24299
25524
|
function resolveFatalRelayRejection(message) {
|
|
24300
|
-
|
|
25525
|
+
const match = /Unexpected server response:\s*(\d{3})\b/u.exec(message);
|
|
25526
|
+
const statusCode = match ? Number.parseInt(match[1], 10) : Number.NaN;
|
|
25527
|
+
return resolveFatalRelayRejectionFromStatus(statusCode);
|
|
25528
|
+
}
|
|
25529
|
+
function resolveFatalRelayRejectionFromStatus(statusCode) {
|
|
25530
|
+
if (!Number.isFinite(statusCode) || ![400, 401, 403, 410, 426].includes(statusCode)) {
|
|
24301
25531
|
return null;
|
|
24302
25532
|
}
|
|
24303
25533
|
return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
|
|
@@ -24308,10 +25538,21 @@ function abortAll(abortControllers) {
|
|
|
24308
25538
|
}
|
|
24309
25539
|
abortControllers.clear();
|
|
24310
25540
|
}
|
|
24311
|
-
function
|
|
24312
|
-
const
|
|
24313
|
-
const
|
|
24314
|
-
|
|
25541
|
+
function readRetryAfterMs(response) {
|
|
25542
|
+
const raw = response.headers["retry-after"];
|
|
25543
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
25544
|
+
if (!value) {
|
|
25545
|
+
return null;
|
|
25546
|
+
}
|
|
25547
|
+
const seconds = Number.parseInt(value, 10);
|
|
25548
|
+
if (Number.isInteger(seconds) && seconds >= 0) {
|
|
25549
|
+
return seconds * 1e3;
|
|
25550
|
+
}
|
|
25551
|
+
const dateMs = Date.parse(value);
|
|
25552
|
+
if (!Number.isFinite(dateMs)) {
|
|
25553
|
+
return null;
|
|
25554
|
+
}
|
|
25555
|
+
return Math.max(0, dateMs - Date.now());
|
|
24315
25556
|
}
|
|
24316
25557
|
async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
|
|
24317
25558
|
const frame = JSON.parse(raw);
|
|
@@ -24432,10 +25673,58 @@ function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
|
|
|
24432
25673
|
};
|
|
24433
25674
|
}
|
|
24434
25675
|
|
|
25676
|
+
// src/relay/status-state.ts
|
|
25677
|
+
async function readRelayStatusSnapshot(paths) {
|
|
25678
|
+
const state = await readLinkState2(paths);
|
|
25679
|
+
return normalizeRelayStatusSnapshot(state.relayStatus);
|
|
25680
|
+
}
|
|
25681
|
+
async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new Date()) {
|
|
25682
|
+
const current = await readLinkState2(paths);
|
|
25683
|
+
await writeJsonFile(paths.stateFile, {
|
|
25684
|
+
...current,
|
|
25685
|
+
relayStatus: {
|
|
25686
|
+
state: status.state,
|
|
25687
|
+
attempt: Number.isFinite(status.attempt) ? Math.max(0, Math.floor(status.attempt)) : 0,
|
|
25688
|
+
message: normalizeMessage(status.message),
|
|
25689
|
+
updatedAt: now.toISOString()
|
|
25690
|
+
}
|
|
25691
|
+
});
|
|
25692
|
+
}
|
|
25693
|
+
async function readLinkState2(paths) {
|
|
25694
|
+
const state = await readJsonFile(paths.stateFile);
|
|
25695
|
+
return state && typeof state === "object" ? state : {};
|
|
25696
|
+
}
|
|
25697
|
+
function normalizeRelayStatusSnapshot(value) {
|
|
25698
|
+
const record = value && typeof value === "object" ? value : null;
|
|
25699
|
+
if (!record || !isRelayStatusState(record.state)) {
|
|
25700
|
+
return null;
|
|
25701
|
+
}
|
|
25702
|
+
const updatedAt = typeof record.updatedAt === "string" && Number.isFinite(Date.parse(record.updatedAt)) ? record.updatedAt : null;
|
|
25703
|
+
if (!updatedAt) {
|
|
25704
|
+
return null;
|
|
25705
|
+
}
|
|
25706
|
+
return {
|
|
25707
|
+
state: record.state,
|
|
25708
|
+
attempt: typeof record.attempt === "number" && Number.isFinite(record.attempt) ? Math.max(0, Math.floor(record.attempt)) : 0,
|
|
25709
|
+
message: normalizeMessage(record.message),
|
|
25710
|
+
updatedAt
|
|
25711
|
+
};
|
|
25712
|
+
}
|
|
25713
|
+
function isRelayStatusState(value) {
|
|
25714
|
+
return value === "connecting" || value === "connected" || value === "disconnected" || value === "retrying" || value === "cooldown" || value === "failed";
|
|
25715
|
+
}
|
|
25716
|
+
function normalizeMessage(value) {
|
|
25717
|
+
if (typeof value !== "string") {
|
|
25718
|
+
return null;
|
|
25719
|
+
}
|
|
25720
|
+
const trimmed = value.trim();
|
|
25721
|
+
return trimmed ? trimmed.slice(0, 240) : null;
|
|
25722
|
+
}
|
|
25723
|
+
|
|
24435
25724
|
// src/runtime/system-info.ts
|
|
24436
25725
|
import { execFileSync } from "child_process";
|
|
24437
25726
|
import { readFileSync } from "fs";
|
|
24438
|
-
import
|
|
25727
|
+
import os5 from "os";
|
|
24439
25728
|
function readLinkSystemInfo() {
|
|
24440
25729
|
const platform = process.platform;
|
|
24441
25730
|
const hostname = readHostname(platform);
|
|
@@ -24474,7 +25763,7 @@ function readHostname(platform) {
|
|
|
24474
25763
|
return computerName;
|
|
24475
25764
|
}
|
|
24476
25765
|
}
|
|
24477
|
-
return normalizeText(
|
|
25766
|
+
return normalizeText(os5.hostname());
|
|
24478
25767
|
}
|
|
24479
25768
|
function readOsLabel(platform) {
|
|
24480
25769
|
if (platform === "darwin") {
|
|
@@ -24482,12 +25771,12 @@ function readOsLabel(platform) {
|
|
|
24482
25771
|
return version ? `macOS ${version}` : "macOS";
|
|
24483
25772
|
}
|
|
24484
25773
|
if (platform === "linux") {
|
|
24485
|
-
return readLinuxOsRelease() ?? `Linux ${
|
|
25774
|
+
return readLinuxOsRelease() ?? `Linux ${os5.release()}`;
|
|
24486
25775
|
}
|
|
24487
25776
|
if (platform === "win32") {
|
|
24488
|
-
return `Windows ${
|
|
25777
|
+
return `Windows ${os5.release()}`;
|
|
24489
25778
|
}
|
|
24490
|
-
return `${
|
|
25779
|
+
return `${os5.type()} ${os5.release()}`.trim();
|
|
24491
25780
|
}
|
|
24492
25781
|
function readLinuxOsRelease() {
|
|
24493
25782
|
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
@@ -24534,11 +25823,11 @@ function truncateText(value, maxLength) {
|
|
|
24534
25823
|
}
|
|
24535
25824
|
|
|
24536
25825
|
// src/topology/network.ts
|
|
24537
|
-
import
|
|
25826
|
+
import os7 from "os";
|
|
24538
25827
|
|
|
24539
25828
|
// src/topology/environment.ts
|
|
24540
25829
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
24541
|
-
import
|
|
25830
|
+
import os6 from "os";
|
|
24542
25831
|
function detectRuntimeEnvironment(env = process.env) {
|
|
24543
25832
|
if (isWsl(env)) {
|
|
24544
25833
|
return {
|
|
@@ -24567,7 +25856,7 @@ function isWsl(env) {
|
|
|
24567
25856
|
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
24568
25857
|
return true;
|
|
24569
25858
|
}
|
|
24570
|
-
const release =
|
|
25859
|
+
const release = os6.release().toLowerCase();
|
|
24571
25860
|
return release.includes("microsoft") || release.includes("wsl");
|
|
24572
25861
|
}
|
|
24573
25862
|
function isContainer(env) {
|
|
@@ -24612,7 +25901,7 @@ async function discoverRouteCandidates(options) {
|
|
|
24612
25901
|
};
|
|
24613
25902
|
}
|
|
24614
25903
|
function discoverLanIps() {
|
|
24615
|
-
return discoverLanIpsFromInterfaces(
|
|
25904
|
+
return discoverLanIpsFromInterfaces(os7.networkInterfaces());
|
|
24616
25905
|
}
|
|
24617
25906
|
function discoverLanIpsFromInterfaces(interfaces) {
|
|
24618
25907
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -24751,7 +26040,7 @@ function unique(values) {
|
|
|
24751
26040
|
// src/link/network-report-state.ts
|
|
24752
26041
|
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
24753
26042
|
async function readNetworkReportState(paths) {
|
|
24754
|
-
const state = await
|
|
26043
|
+
const state = await readLinkState3(paths);
|
|
24755
26044
|
return normalizeNetworkReportState(state.networkReport);
|
|
24756
26045
|
}
|
|
24757
26046
|
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
@@ -24818,14 +26107,14 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
|
24818
26107
|
};
|
|
24819
26108
|
}
|
|
24820
26109
|
async function updateNetworkReportState(paths, update) {
|
|
24821
|
-
const state = await
|
|
26110
|
+
const state = await readLinkState3(paths);
|
|
24822
26111
|
const next = {
|
|
24823
26112
|
...state,
|
|
24824
26113
|
networkReport: update(normalizeNetworkReportState(state.networkReport))
|
|
24825
26114
|
};
|
|
24826
26115
|
await writeJsonFile(paths.stateFile, next);
|
|
24827
26116
|
}
|
|
24828
|
-
async function
|
|
26117
|
+
async function readLinkState3(paths) {
|
|
24829
26118
|
const state = await readJsonFile(paths.stateFile);
|
|
24830
26119
|
return state && typeof state === "object" ? state : {};
|
|
24831
26120
|
}
|
|
@@ -25098,6 +26387,89 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
25098
26387
|
}
|
|
25099
26388
|
}
|
|
25100
26389
|
|
|
26390
|
+
// src/daemon/process-guard.ts
|
|
26391
|
+
var installed = false;
|
|
26392
|
+
var fatalShutdownInProgress = false;
|
|
26393
|
+
var activeLogger = null;
|
|
26394
|
+
var activeOptions = {};
|
|
26395
|
+
var DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
26396
|
+
function installDaemonProcessGuard(logger, options = {}) {
|
|
26397
|
+
activeLogger = logger;
|
|
26398
|
+
activeOptions = options;
|
|
26399
|
+
if (installed) {
|
|
26400
|
+
return;
|
|
26401
|
+
}
|
|
26402
|
+
installed = true;
|
|
26403
|
+
process.on("unhandledRejection", (reason) => {
|
|
26404
|
+
void handleFatalProcessFailure("process_unhandled_rejection", reason);
|
|
26405
|
+
});
|
|
26406
|
+
process.on("uncaughtException", (error) => {
|
|
26407
|
+
void handleFatalProcessFailure("process_uncaught_exception", error);
|
|
26408
|
+
});
|
|
26409
|
+
}
|
|
26410
|
+
async function handleFatalProcessFailure(event, error) {
|
|
26411
|
+
const fields = describeProcessFailure(error);
|
|
26412
|
+
const logger = activeLogger;
|
|
26413
|
+
const options = activeOptions;
|
|
26414
|
+
if (fatalShutdownInProgress) {
|
|
26415
|
+
writeFatalFailureToStderr(event, fields);
|
|
26416
|
+
return;
|
|
26417
|
+
}
|
|
26418
|
+
fatalShutdownInProgress = true;
|
|
26419
|
+
if (logger) {
|
|
26420
|
+
try {
|
|
26421
|
+
await logger.error(event, fields);
|
|
26422
|
+
await logger.flush();
|
|
26423
|
+
} catch {
|
|
26424
|
+
}
|
|
26425
|
+
}
|
|
26426
|
+
writeFatalFailureToStderr(event, fields);
|
|
26427
|
+
if (options.onFatal) {
|
|
26428
|
+
try {
|
|
26429
|
+
await Promise.race([
|
|
26430
|
+
options.onFatal(),
|
|
26431
|
+
wait(options.shutdownTimeoutMs ?? DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS)
|
|
26432
|
+
]);
|
|
26433
|
+
} catch (shutdownError) {
|
|
26434
|
+
if (logger) {
|
|
26435
|
+
try {
|
|
26436
|
+
await logger.error("process_fatal_shutdown_failed", {
|
|
26437
|
+
error: shutdownError instanceof Error ? shutdownError.message : String(shutdownError)
|
|
26438
|
+
});
|
|
26439
|
+
await logger.flush();
|
|
26440
|
+
} catch {
|
|
26441
|
+
}
|
|
26442
|
+
}
|
|
26443
|
+
}
|
|
26444
|
+
}
|
|
26445
|
+
process.exit(1);
|
|
26446
|
+
}
|
|
26447
|
+
function describeProcessFailure(error) {
|
|
26448
|
+
if (error instanceof Error) {
|
|
26449
|
+
return {
|
|
26450
|
+
message: error.message,
|
|
26451
|
+
...error.stack ? { stack: error.stack } : {}
|
|
26452
|
+
};
|
|
26453
|
+
}
|
|
26454
|
+
return { message: String(error) };
|
|
26455
|
+
}
|
|
26456
|
+
function writeFatalFailureToStderr(event, fields) {
|
|
26457
|
+
try {
|
|
26458
|
+
process.stderr.write(
|
|
26459
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${fields.message}
|
|
26460
|
+
${fields.stack ?? ""}
|
|
26461
|
+
`
|
|
26462
|
+
);
|
|
26463
|
+
} catch {
|
|
26464
|
+
}
|
|
26465
|
+
}
|
|
26466
|
+
function wait(ms) {
|
|
26467
|
+
return new Promise((resolve) => {
|
|
26468
|
+
const timer = setTimeout(resolve, ms);
|
|
26469
|
+
timer.unref?.();
|
|
26470
|
+
});
|
|
26471
|
+
}
|
|
26472
|
+
|
|
25101
26473
|
// src/daemon/scheduler.ts
|
|
25102
26474
|
function startCronDeliveryScheduler(options) {
|
|
25103
26475
|
let running = false;
|
|
@@ -25183,6 +26555,11 @@ async function startLinkService(options = {}) {
|
|
|
25183
26555
|
current_version: migration.currentVersion
|
|
25184
26556
|
});
|
|
25185
26557
|
}
|
|
26558
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
26559
|
+
paths,
|
|
26560
|
+
logger,
|
|
26561
|
+
source: "service_startup"
|
|
26562
|
+
});
|
|
25186
26563
|
const conversations = new ConversationService(paths, logger);
|
|
25187
26564
|
await conversations.rebuildStatisticsIndex();
|
|
25188
26565
|
let relay = null;
|
|
@@ -25214,6 +26591,11 @@ async function startLinkService(options = {}) {
|
|
|
25214
26591
|
logger,
|
|
25215
26592
|
conversations,
|
|
25216
26593
|
onPairingClaimed: async () => {
|
|
26594
|
+
void ensureHermesLinkSkillInstalledBestEffort({
|
|
26595
|
+
paths,
|
|
26596
|
+
logger,
|
|
26597
|
+
source: "pairing_claimed"
|
|
26598
|
+
});
|
|
25217
26599
|
triggerHermesSessionSync();
|
|
25218
26600
|
void loadRelayStreamBatchPolicy("pairing_claimed");
|
|
25219
26601
|
await options.onPairingClaimed?.();
|
|
@@ -25238,6 +26620,38 @@ async function startLinkService(options = {}) {
|
|
|
25238
26620
|
error: error.message
|
|
25239
26621
|
});
|
|
25240
26622
|
});
|
|
26623
|
+
server.on("clientError", (error, socket) => {
|
|
26624
|
+
if (isExpectedClientDisconnectError3(error, {
|
|
26625
|
+
sse: isActiveSseSocket(socket)
|
|
26626
|
+
})) {
|
|
26627
|
+
socket.destroy();
|
|
26628
|
+
return;
|
|
26629
|
+
}
|
|
26630
|
+
void logger.warn("client_connection_error", {
|
|
26631
|
+
port: config.port,
|
|
26632
|
+
link_id: identity?.link_id ?? null,
|
|
26633
|
+
error: error.message
|
|
26634
|
+
});
|
|
26635
|
+
if (socket.writable) {
|
|
26636
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
26637
|
+
} else {
|
|
26638
|
+
socket.destroy();
|
|
26639
|
+
}
|
|
26640
|
+
});
|
|
26641
|
+
server.on("connection", (socket) => {
|
|
26642
|
+
socket.on("error", (error) => {
|
|
26643
|
+
if (isExpectedClientDisconnectError3(error, {
|
|
26644
|
+
sse: isActiveSseSocket(socket)
|
|
26645
|
+
})) {
|
|
26646
|
+
return;
|
|
26647
|
+
}
|
|
26648
|
+
void logger.warn("socket_error", {
|
|
26649
|
+
port: config.port,
|
|
26650
|
+
link_id: identity?.link_id ?? null,
|
|
26651
|
+
error: error.message
|
|
26652
|
+
});
|
|
26653
|
+
});
|
|
26654
|
+
});
|
|
25241
26655
|
void logger.info("service_started", {
|
|
25242
26656
|
port: config.port,
|
|
25243
26657
|
link_id: identity?.link_id ?? null
|
|
@@ -25263,9 +26677,8 @@ async function startLinkService(options = {}) {
|
|
|
25263
26677
|
relayBaseUrl: config.relayBaseUrl,
|
|
25264
26678
|
linkId: identity.link_id,
|
|
25265
26679
|
localPort: config.port,
|
|
25266
|
-
|
|
25267
|
-
|
|
25268
|
-
backoffMaxMs: 3e4,
|
|
26680
|
+
paths,
|
|
26681
|
+
maxReconnectAttempts: options.relayMaxReconnectAttempts,
|
|
25269
26682
|
onStreamBatchPolicy: (policy) => {
|
|
25270
26683
|
void logger.info("relay_stream_policy_updated", {
|
|
25271
26684
|
flushIntervalMs: policy.flushIntervalMs,
|
|
@@ -25273,6 +26686,7 @@ async function startLinkService(options = {}) {
|
|
|
25273
26686
|
});
|
|
25274
26687
|
},
|
|
25275
26688
|
onStatus: (status) => {
|
|
26689
|
+
void writeRelayStatusSnapshot(paths, status).catch(() => void 0);
|
|
25276
26690
|
void logger.info("relay_status", status);
|
|
25277
26691
|
if (status.state === "connected") {
|
|
25278
26692
|
const now = Date.now();
|
|
@@ -25313,8 +26727,13 @@ async function startLinkService(options = {}) {
|
|
|
25313
26727
|
if (options.writePidFile) {
|
|
25314
26728
|
await writePidFile(paths);
|
|
25315
26729
|
}
|
|
25316
|
-
|
|
26730
|
+
let closed = false;
|
|
26731
|
+
const service = {
|
|
25317
26732
|
async close() {
|
|
26733
|
+
if (closed) {
|
|
26734
|
+
return;
|
|
26735
|
+
}
|
|
26736
|
+
closed = true;
|
|
25318
26737
|
relay?.close();
|
|
25319
26738
|
await closeServer(server);
|
|
25320
26739
|
await Promise.all([
|
|
@@ -25330,6 +26749,12 @@ async function startLinkService(options = {}) {
|
|
|
25330
26749
|
}
|
|
25331
26750
|
}
|
|
25332
26751
|
};
|
|
26752
|
+
if (options.writePidFile) {
|
|
26753
|
+
installDaemonProcessGuard(logger, {
|
|
26754
|
+
onFatal: () => service.close()
|
|
26755
|
+
});
|
|
26756
|
+
}
|
|
26757
|
+
return service;
|
|
25333
26758
|
}
|
|
25334
26759
|
function waitForRelayReadyTimeout(timeoutMs) {
|
|
25335
26760
|
return new Promise((resolve) => {
|
|
@@ -25384,6 +26809,16 @@ async function closeServer(server) {
|
|
|
25384
26809
|
server.closeIdleConnections?.();
|
|
25385
26810
|
});
|
|
25386
26811
|
}
|
|
26812
|
+
function isExpectedClientDisconnectError3(error, options = {}) {
|
|
26813
|
+
if (!(error instanceof Error)) {
|
|
26814
|
+
return false;
|
|
26815
|
+
}
|
|
26816
|
+
const code = String(error.code ?? "");
|
|
26817
|
+
if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted)/iu.test(error.message)) {
|
|
26818
|
+
return true;
|
|
26819
|
+
}
|
|
26820
|
+
return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
|
|
26821
|
+
}
|
|
25387
26822
|
async function listenServer(server, port) {
|
|
25388
26823
|
await new Promise((resolve, reject) => {
|
|
25389
26824
|
const cleanup = () => {
|
|
@@ -25405,6 +26840,16 @@ async function listenServer(server, port) {
|
|
|
25405
26840
|
}
|
|
25406
26841
|
|
|
25407
26842
|
// src/daemon/process.ts
|
|
26843
|
+
var SUPERVISOR_RESTART_INITIAL_DELAY_MS = 1e3;
|
|
26844
|
+
var SUPERVISOR_RESTART_MAX_DELAY_MS = 3e4;
|
|
26845
|
+
var SUPERVISOR_STABLE_UPTIME_MS = 6e4;
|
|
26846
|
+
var SUPERVISOR_HEALTH_STARTUP_GRACE_MS = 15e3;
|
|
26847
|
+
var SUPERVISOR_HEALTH_INTERVAL_MS = 15e3;
|
|
26848
|
+
var SUPERVISOR_HEALTH_TIMEOUT_MS = 3e3;
|
|
26849
|
+
var SUPERVISOR_HEALTH_FAILURE_THRESHOLD = 3;
|
|
26850
|
+
var SUPERVISOR_CHILD_STOP_TIMEOUT_MS = 5e3;
|
|
26851
|
+
var SUPERVISOR_STOP_INTENT_TTL_MS = 2 * 6e4;
|
|
26852
|
+
var INTERNAL_HEALTH_PROBE_HEADER2 = "x-hermes-link-internal-health-probe";
|
|
25408
26853
|
async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
25409
26854
|
const config = await loadConfig(paths);
|
|
25410
26855
|
let status = await getDaemonStatus(paths);
|
|
@@ -25429,7 +26874,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25429
26874
|
});
|
|
25430
26875
|
child.unref();
|
|
25431
26876
|
for (let index = 0; index < 12; index += 1) {
|
|
25432
|
-
await
|
|
26877
|
+
await wait2(250);
|
|
25433
26878
|
const next = await getDaemonStatus(paths);
|
|
25434
26879
|
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
25435
26880
|
return next;
|
|
@@ -25441,43 +26886,92 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
|
25441
26886
|
await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
|
|
25442
26887
|
const log = createRotatingTextLogWriter({
|
|
25443
26888
|
paths,
|
|
25444
|
-
fileName:
|
|
26889
|
+
fileName: path26.basename(daemonLogFile(paths))
|
|
25445
26890
|
});
|
|
25446
26891
|
const scriptPath = currentCliScriptPath();
|
|
25447
|
-
const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
25448
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
25449
|
-
env: process.env
|
|
25450
|
-
});
|
|
25451
26892
|
const write = (chunk) => {
|
|
25452
26893
|
void log.write(chunk);
|
|
25453
26894
|
};
|
|
25454
26895
|
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
|
|
25455
26896
|
`);
|
|
25456
|
-
|
|
25457
|
-
|
|
26897
|
+
await clearExpiredSupervisorStopIntent(paths).catch(() => void 0);
|
|
26898
|
+
const logSupervisorUnhandledRejection = (reason) => {
|
|
26899
|
+
writeSupervisorFailure(write, "supervisor_unhandled_rejection", reason);
|
|
26900
|
+
};
|
|
26901
|
+
const logSupervisorUncaughtException = (error) => {
|
|
26902
|
+
writeSupervisorFailure(write, "supervisor_uncaught_exception", error);
|
|
26903
|
+
};
|
|
26904
|
+
let child = null;
|
|
26905
|
+
let stopRequested = false;
|
|
25458
26906
|
const forwardStop = () => {
|
|
25459
|
-
|
|
26907
|
+
stopRequested = true;
|
|
26908
|
+
if (child?.pid && isProcessAlive3(child.pid)) {
|
|
25460
26909
|
child.kill("SIGTERM");
|
|
25461
26910
|
}
|
|
25462
26911
|
};
|
|
25463
26912
|
process.once("SIGINT", forwardStop);
|
|
25464
26913
|
process.once("SIGTERM", forwardStop);
|
|
25465
|
-
|
|
25466
|
-
|
|
25467
|
-
|
|
25468
|
-
|
|
25469
|
-
|
|
25470
|
-
|
|
25471
|
-
|
|
25472
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
26914
|
+
process.on("unhandledRejection", logSupervisorUnhandledRejection);
|
|
26915
|
+
process.on("uncaughtException", logSupervisorUncaughtException);
|
|
26916
|
+
let restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
|
|
26917
|
+
let finalResult = {
|
|
26918
|
+
code: 0,
|
|
26919
|
+
signal: null
|
|
26920
|
+
};
|
|
26921
|
+
try {
|
|
26922
|
+
while (!stopRequested) {
|
|
26923
|
+
const startedAt = Date.now();
|
|
26924
|
+
child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
26925
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26926
|
+
env: process.env
|
|
26927
|
+
});
|
|
26928
|
+
const childPid = child.pid ?? null;
|
|
26929
|
+
child.stdout?.on("data", write);
|
|
26930
|
+
child.stderr?.on("data", write);
|
|
26931
|
+
const healthMonitor = startSupervisorHealthMonitor(paths, child, write);
|
|
26932
|
+
const result = await new Promise((resolve, reject) => {
|
|
26933
|
+
child?.once("error", reject);
|
|
26934
|
+
child?.once("exit", (code, signal) => resolve({ code, signal }));
|
|
26935
|
+
}).catch((error) => {
|
|
26936
|
+
write(
|
|
26937
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child failed to start: ${error instanceof Error ? error.message : String(error)}
|
|
26938
|
+
`
|
|
26939
|
+
);
|
|
26940
|
+
return { code: 1, signal: null };
|
|
26941
|
+
});
|
|
26942
|
+
healthMonitor.close();
|
|
26943
|
+
finalResult = result;
|
|
26944
|
+
child = null;
|
|
26945
|
+
const expectedStop = childPid !== null && await consumeSupervisorStopIntent(paths, childPid);
|
|
26946
|
+
if (stopRequested || expectedStop || result.code === 0 || isIntentionalStopSignal(result.signal)) {
|
|
26947
|
+
break;
|
|
26948
|
+
}
|
|
26949
|
+
const uptimeMs = Date.now() - startedAt;
|
|
26950
|
+
if (uptimeMs >= SUPERVISOR_STABLE_UPTIME_MS) {
|
|
26951
|
+
restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
|
|
26952
|
+
}
|
|
26953
|
+
write(
|
|
26954
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child stopped unexpectedly code=${result.code ?? "null"} signal=${result.signal ?? "null"}; restarting in ${restartDelayMs}ms
|
|
26955
|
+
`
|
|
26956
|
+
);
|
|
26957
|
+
await wait2(restartDelayMs);
|
|
26958
|
+
restartDelayMs = Math.min(
|
|
26959
|
+
restartDelayMs * 2,
|
|
26960
|
+
SUPERVISOR_RESTART_MAX_DELAY_MS
|
|
26961
|
+
);
|
|
26962
|
+
}
|
|
26963
|
+
} finally {
|
|
26964
|
+
process.off("SIGINT", forwardStop);
|
|
26965
|
+
process.off("SIGTERM", forwardStop);
|
|
26966
|
+
process.off("unhandledRejection", logSupervisorUnhandledRejection);
|
|
26967
|
+
process.off("uncaughtException", logSupervisorUncaughtException);
|
|
26968
|
+
}
|
|
25475
26969
|
write(
|
|
25476
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${
|
|
26970
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${finalResult.code ?? "null"} signal=${finalResult.signal ?? "null"}
|
|
25477
26971
|
`
|
|
25478
26972
|
);
|
|
25479
26973
|
await log.flush();
|
|
25480
|
-
return
|
|
26974
|
+
return finalResult.code ?? (finalResult.signal ? 0 : 1);
|
|
25481
26975
|
}
|
|
25482
26976
|
async function probeLocalLinkService(options) {
|
|
25483
26977
|
const unreachable = {
|
|
@@ -25515,6 +27009,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25515
27009
|
if (!status.running || !status.pid) {
|
|
25516
27010
|
return status;
|
|
25517
27011
|
}
|
|
27012
|
+
await writeSupervisorStopIntent(paths, status.pid).catch(() => void 0);
|
|
25518
27013
|
try {
|
|
25519
27014
|
process.kill(status.pid, "SIGTERM");
|
|
25520
27015
|
} catch {
|
|
@@ -25522,7 +27017,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25522
27017
|
return await getDaemonStatus(paths);
|
|
25523
27018
|
}
|
|
25524
27019
|
for (let index = 0; index < 20; index += 1) {
|
|
25525
|
-
await
|
|
27020
|
+
await wait2(250);
|
|
25526
27021
|
if (!isProcessAlive3(status.pid)) {
|
|
25527
27022
|
break;
|
|
25528
27023
|
}
|
|
@@ -25533,7 +27028,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25533
27028
|
} catch {
|
|
25534
27029
|
}
|
|
25535
27030
|
for (let index = 0; index < 10; index += 1) {
|
|
25536
|
-
await
|
|
27031
|
+
await wait2(250);
|
|
25537
27032
|
if (!isProcessAlive3(status.pid)) {
|
|
25538
27033
|
break;
|
|
25539
27034
|
}
|
|
@@ -25570,7 +27065,7 @@ function currentCliScriptPath() {
|
|
|
25570
27065
|
return process.argv[1];
|
|
25571
27066
|
}
|
|
25572
27067
|
async function readPid(filePath) {
|
|
25573
|
-
const raw = await
|
|
27068
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
25574
27069
|
if (!raw) {
|
|
25575
27070
|
return null;
|
|
25576
27071
|
}
|
|
@@ -25585,6 +27080,171 @@ function isProcessAlive3(pid) {
|
|
|
25585
27080
|
return false;
|
|
25586
27081
|
}
|
|
25587
27082
|
}
|
|
27083
|
+
function isIntentionalStopSignal(signal) {
|
|
27084
|
+
return signal === "SIGINT" || signal === "SIGTERM" || signal === "SIGKILL";
|
|
27085
|
+
}
|
|
27086
|
+
function startSupervisorHealthMonitor(paths, child, write) {
|
|
27087
|
+
let closed = false;
|
|
27088
|
+
let failureCount = 0;
|
|
27089
|
+
let timer = null;
|
|
27090
|
+
let forceKillTimer = null;
|
|
27091
|
+
const schedule = (delayMs) => {
|
|
27092
|
+
timer = setTimeout(check, delayMs);
|
|
27093
|
+
timer.unref?.();
|
|
27094
|
+
};
|
|
27095
|
+
const check = () => {
|
|
27096
|
+
void probeSupervisorHttpHealth(paths).then((healthy) => {
|
|
27097
|
+
if (closed) {
|
|
27098
|
+
return;
|
|
27099
|
+
}
|
|
27100
|
+
if (healthy) {
|
|
27101
|
+
failureCount = 0;
|
|
27102
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27103
|
+
return;
|
|
27104
|
+
}
|
|
27105
|
+
failureCount += 1;
|
|
27106
|
+
if (failureCount < SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
|
|
27107
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27108
|
+
return;
|
|
27109
|
+
}
|
|
27110
|
+
closed = true;
|
|
27111
|
+
write(
|
|
27112
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed ${failureCount} consecutive times; restarting child
|
|
27113
|
+
`
|
|
27114
|
+
);
|
|
27115
|
+
terminateChild(child, forceKillTimer);
|
|
27116
|
+
forceKillTimer = setTimeout(() => {
|
|
27117
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27118
|
+
child.kill("SIGKILL");
|
|
27119
|
+
}
|
|
27120
|
+
}, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
|
|
27121
|
+
forceKillTimer.unref?.();
|
|
27122
|
+
}).catch((error) => {
|
|
27123
|
+
if (closed) {
|
|
27124
|
+
return;
|
|
27125
|
+
}
|
|
27126
|
+
failureCount += 1;
|
|
27127
|
+
write(
|
|
27128
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed: ${error instanceof Error ? error.message : String(error)}
|
|
27129
|
+
`
|
|
27130
|
+
);
|
|
27131
|
+
if (failureCount >= SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
|
|
27132
|
+
closed = true;
|
|
27133
|
+
terminateChild(child, forceKillTimer);
|
|
27134
|
+
forceKillTimer = setTimeout(() => {
|
|
27135
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27136
|
+
child.kill("SIGKILL");
|
|
27137
|
+
}
|
|
27138
|
+
}, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
|
|
27139
|
+
forceKillTimer.unref?.();
|
|
27140
|
+
return;
|
|
27141
|
+
}
|
|
27142
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27143
|
+
});
|
|
27144
|
+
};
|
|
27145
|
+
schedule(SUPERVISOR_HEALTH_STARTUP_GRACE_MS);
|
|
27146
|
+
return {
|
|
27147
|
+
close() {
|
|
27148
|
+
closed = true;
|
|
27149
|
+
if (timer) {
|
|
27150
|
+
clearTimeout(timer);
|
|
27151
|
+
timer = null;
|
|
27152
|
+
}
|
|
27153
|
+
if (forceKillTimer) {
|
|
27154
|
+
clearTimeout(forceKillTimer);
|
|
27155
|
+
forceKillTimer = null;
|
|
27156
|
+
}
|
|
27157
|
+
}
|
|
27158
|
+
};
|
|
27159
|
+
}
|
|
27160
|
+
async function probeSupervisorHttpHealth(paths) {
|
|
27161
|
+
const config = await loadConfig(paths).catch(() => null);
|
|
27162
|
+
if (!config) {
|
|
27163
|
+
return false;
|
|
27164
|
+
}
|
|
27165
|
+
try {
|
|
27166
|
+
const response = await fetch(`http://127.0.0.1:${config.port}/api/v1/bootstrap`, {
|
|
27167
|
+
headers: {
|
|
27168
|
+
accept: "application/json",
|
|
27169
|
+
[INTERNAL_HEALTH_PROBE_HEADER2]: "1"
|
|
27170
|
+
},
|
|
27171
|
+
signal: AbortSignal.timeout(SUPERVISOR_HEALTH_TIMEOUT_MS)
|
|
27172
|
+
});
|
|
27173
|
+
return response.ok;
|
|
27174
|
+
} catch {
|
|
27175
|
+
return false;
|
|
27176
|
+
}
|
|
27177
|
+
}
|
|
27178
|
+
function terminateChild(child, previousForceKillTimer) {
|
|
27179
|
+
if (previousForceKillTimer) {
|
|
27180
|
+
clearTimeout(previousForceKillTimer);
|
|
27181
|
+
}
|
|
27182
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27183
|
+
child.kill("SIGTERM");
|
|
27184
|
+
}
|
|
27185
|
+
}
|
|
27186
|
+
function supervisorStopIntentPath(paths) {
|
|
27187
|
+
return path26.join(paths.runDir, "supervisor-stop-intent.json");
|
|
27188
|
+
}
|
|
27189
|
+
async function writeSupervisorStopIntent(paths, pid) {
|
|
27190
|
+
await mkdir14(paths.runDir, { recursive: true, mode: 448 });
|
|
27191
|
+
await writeFile4(
|
|
27192
|
+
supervisorStopIntentPath(paths),
|
|
27193
|
+
`${JSON.stringify({ pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
27194
|
+
`,
|
|
27195
|
+
{ mode: 384 }
|
|
27196
|
+
);
|
|
27197
|
+
}
|
|
27198
|
+
async function consumeSupervisorStopIntent(paths, pid) {
|
|
27199
|
+
const filePath = supervisorStopIntentPath(paths);
|
|
27200
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
27201
|
+
if (!raw) {
|
|
27202
|
+
return false;
|
|
27203
|
+
}
|
|
27204
|
+
const payload = parseSupervisorStopIntent(raw);
|
|
27205
|
+
if (!isValidSupervisorStopIntent(payload)) {
|
|
27206
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27207
|
+
return false;
|
|
27208
|
+
}
|
|
27209
|
+
if (payload.pid !== pid) {
|
|
27210
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27211
|
+
return false;
|
|
27212
|
+
}
|
|
27213
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27214
|
+
return true;
|
|
27215
|
+
}
|
|
27216
|
+
async function clearExpiredSupervisorStopIntent(paths) {
|
|
27217
|
+
const filePath = supervisorStopIntentPath(paths);
|
|
27218
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
27219
|
+
if (!raw) {
|
|
27220
|
+
return;
|
|
27221
|
+
}
|
|
27222
|
+
const payload = parseSupervisorStopIntent(raw);
|
|
27223
|
+
if (!isValidSupervisorStopIntent(payload)) {
|
|
27224
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27225
|
+
}
|
|
27226
|
+
}
|
|
27227
|
+
function parseSupervisorStopIntent(raw) {
|
|
27228
|
+
try {
|
|
27229
|
+
return JSON.parse(raw);
|
|
27230
|
+
} catch {
|
|
27231
|
+
return {};
|
|
27232
|
+
}
|
|
27233
|
+
}
|
|
27234
|
+
function isValidSupervisorStopIntent(payload) {
|
|
27235
|
+
if (typeof payload.pid !== "number" || !Number.isInteger(payload.pid) || typeof payload.created_at !== "string") {
|
|
27236
|
+
return false;
|
|
27237
|
+
}
|
|
27238
|
+
const createdAtMs = Date.parse(payload.created_at);
|
|
27239
|
+
return Number.isFinite(createdAtMs) && Date.now() - createdAtMs <= SUPERVISOR_STOP_INTENT_TTL_MS;
|
|
27240
|
+
}
|
|
27241
|
+
function writeSupervisorFailure(write, event, error) {
|
|
27242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27243
|
+
const stack = error instanceof Error && error.stack ? `
|
|
27244
|
+
${error.stack}` : "";
|
|
27245
|
+
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${message}${stack}
|
|
27246
|
+
`);
|
|
27247
|
+
}
|
|
25588
27248
|
async function pidBackedServiceIsReachable(paths) {
|
|
25589
27249
|
const config = await loadConfig(paths).catch(() => null);
|
|
25590
27250
|
if (!config) {
|
|
@@ -25592,7 +27252,7 @@ async function pidBackedServiceIsReachable(paths) {
|
|
|
25592
27252
|
}
|
|
25593
27253
|
return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
|
|
25594
27254
|
}
|
|
25595
|
-
function
|
|
27255
|
+
function wait2(ms) {
|
|
25596
27256
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25597
27257
|
}
|
|
25598
27258
|
|
|
@@ -25999,7 +27659,7 @@ async function readRemoteLinkPolicy(options) {
|
|
|
25999
27659
|
}
|
|
26000
27660
|
}
|
|
26001
27661
|
function normalizeServerSnapshot(payload) {
|
|
26002
|
-
const snapshot =
|
|
27662
|
+
const snapshot = toRecord20(payload);
|
|
26003
27663
|
const policy = toNullableRecord2(snapshot.policy);
|
|
26004
27664
|
if (!policy) {
|
|
26005
27665
|
return {
|
|
@@ -26342,7 +28002,7 @@ async function writeUpdateState2(paths, state) {
|
|
|
26342
28002
|
await writeJsonFile(updateStatePath2(paths), state);
|
|
26343
28003
|
}
|
|
26344
28004
|
async function readUpdateLogLines2(paths) {
|
|
26345
|
-
const raw = await
|
|
28005
|
+
const raw = await readFile20(updateLogPath2(paths), "utf8").catch(() => "");
|
|
26346
28006
|
if (!raw.trim()) {
|
|
26347
28007
|
return [];
|
|
26348
28008
|
}
|
|
@@ -26351,10 +28011,10 @@ async function readUpdateLogLines2(paths) {
|
|
|
26351
28011
|
);
|
|
26352
28012
|
}
|
|
26353
28013
|
function updateStatePath2(paths) {
|
|
26354
|
-
return
|
|
28014
|
+
return path27.join(paths.runDir, "link-update-state.json");
|
|
26355
28015
|
}
|
|
26356
28016
|
function updateLogPath2(paths) {
|
|
26357
|
-
return
|
|
28017
|
+
return path27.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
26358
28018
|
}
|
|
26359
28019
|
async function clearUpdateLogFiles2(paths) {
|
|
26360
28020
|
const primary = updateLogPath2(paths);
|
|
@@ -26426,7 +28086,7 @@ function isProcessAlive4(pid) {
|
|
|
26426
28086
|
return false;
|
|
26427
28087
|
}
|
|
26428
28088
|
}
|
|
26429
|
-
function
|
|
28089
|
+
function toRecord20(value) {
|
|
26430
28090
|
return typeof value === "object" && value !== null ? value : {};
|
|
26431
28091
|
}
|
|
26432
28092
|
function toNullableRecord2(value) {
|
|
@@ -26438,7 +28098,7 @@ function readString20(payload, key) {
|
|
|
26438
28098
|
}
|
|
26439
28099
|
|
|
26440
28100
|
// src/pairing/pairing.ts
|
|
26441
|
-
import
|
|
28101
|
+
import path28 from "path";
|
|
26442
28102
|
import { rm as rm11 } from "fs/promises";
|
|
26443
28103
|
|
|
26444
28104
|
// src/relay/bootstrap.ts
|
|
@@ -26778,10 +28438,10 @@ async function loadRequiredIdentity2(paths) {
|
|
|
26778
28438
|
}
|
|
26779
28439
|
return identity;
|
|
26780
28440
|
}
|
|
26781
|
-
async function postServerJson(serverBaseUrl,
|
|
28441
|
+
async function postServerJson(serverBaseUrl, path29, body, options) {
|
|
26782
28442
|
let response;
|
|
26783
28443
|
try {
|
|
26784
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
28444
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
|
|
26785
28445
|
method: "POST",
|
|
26786
28446
|
headers: {
|
|
26787
28447
|
accept: "application/json",
|
|
@@ -26829,10 +28489,10 @@ function pairingErrorSnapshot(stage, error) {
|
|
|
26829
28489
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
26830
28490
|
};
|
|
26831
28491
|
}
|
|
26832
|
-
async function patchServerJson(serverBaseUrl,
|
|
28492
|
+
async function patchServerJson(serverBaseUrl, path29, token, body, options) {
|
|
26833
28493
|
let response;
|
|
26834
28494
|
try {
|
|
26835
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
28495
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
|
|
26836
28496
|
method: "PATCH",
|
|
26837
28497
|
headers: {
|
|
26838
28498
|
accept: "application/json",
|
|
@@ -26880,10 +28540,10 @@ function createPairingNetworkError(input) {
|
|
|
26880
28540
|
);
|
|
26881
28541
|
}
|
|
26882
28542
|
function pairingClaimPath(sessionId, paths) {
|
|
26883
|
-
return
|
|
28543
|
+
return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
26884
28544
|
}
|
|
26885
28545
|
function pairingSessionPath(sessionId, paths) {
|
|
26886
|
-
return
|
|
28546
|
+
return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
26887
28547
|
}
|
|
26888
28548
|
function qrPreferredUrls(routes) {
|
|
26889
28549
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -26997,7 +28657,12 @@ function registerSystemRoutes(router, options) {
|
|
|
26997
28657
|
error: error instanceof Error ? error.message : String(error)
|
|
26998
28658
|
});
|
|
26999
28659
|
});
|
|
27000
|
-
void options.onPairingClaimed?.()
|
|
28660
|
+
void Promise.resolve().then(() => options.onPairingClaimed?.()).catch((error) => {
|
|
28661
|
+
void logger.warn("pairing_claim_callback_failed", {
|
|
28662
|
+
session_id: sessionId,
|
|
28663
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28664
|
+
});
|
|
28665
|
+
});
|
|
27001
28666
|
}, 250);
|
|
27002
28667
|
timer.unref?.();
|
|
27003
28668
|
});
|
|
@@ -28035,6 +29700,18 @@ async function createApp(options = {}) {
|
|
|
28035
29700
|
};
|
|
28036
29701
|
const app = new Koa();
|
|
28037
29702
|
const router = new Router();
|
|
29703
|
+
app.on("error", (error, ctx) => {
|
|
29704
|
+
if (isExpectedClientDisconnectError2(error, {
|
|
29705
|
+
sse: isSseRequestContext(ctx)
|
|
29706
|
+
})) {
|
|
29707
|
+
return;
|
|
29708
|
+
}
|
|
29709
|
+
void logger.error("http_app_error", {
|
|
29710
|
+
method: ctx?.method ?? null,
|
|
29711
|
+
path: ctx?.path ?? null,
|
|
29712
|
+
error: error instanceof Error ? error.message : String(error)
|
|
29713
|
+
});
|
|
29714
|
+
});
|
|
28038
29715
|
app.use(createHttpErrorMiddleware(logger));
|
|
28039
29716
|
registerSystemRoutes(router, {
|
|
28040
29717
|
paths,
|
|
@@ -28073,6 +29750,11 @@ export {
|
|
|
28073
29750
|
resolveRuntimePaths,
|
|
28074
29751
|
createFileLogger,
|
|
28075
29752
|
getLinkLogFile,
|
|
29753
|
+
readRecentLogEntries,
|
|
29754
|
+
readRecentTextLogEntries,
|
|
29755
|
+
getGatewayLogFiles,
|
|
29756
|
+
readRecentGatewayLogEntries,
|
|
29757
|
+
flushLogFiles,
|
|
28076
29758
|
ensureHermesApiServerAvailable,
|
|
28077
29759
|
readHermesVersion,
|
|
28078
29760
|
defaultLinkConfig,
|
|
@@ -28085,6 +29767,7 @@ export {
|
|
|
28085
29767
|
getIdentityStatus,
|
|
28086
29768
|
ConversationService,
|
|
28087
29769
|
hasActiveDevices,
|
|
29770
|
+
ensureHermesLinkSkillInstalledBestEffort,
|
|
28088
29771
|
detectRuntimeEnvironment,
|
|
28089
29772
|
preparePairing,
|
|
28090
29773
|
readPairingClaim,
|
|
@@ -28092,6 +29775,7 @@ export {
|
|
|
28092
29775
|
createApp,
|
|
28093
29776
|
fetchRelayStreamBatchPolicy,
|
|
28094
29777
|
connectRelayControl,
|
|
29778
|
+
readRelayStatusSnapshot,
|
|
28095
29779
|
reportLinkStatusToServer,
|
|
28096
29780
|
startLinkService,
|
|
28097
29781
|
startDaemonProcess,
|