@hermespilot/link 0.5.9 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/{chunk-52SUJB7K.js → chunk-RBMFF32Z.js} +2268 -478
- package/dist/cli/index.js +615 -25
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
4868
|
+
var LINK_VERSION = "0.6.1";
|
|
4506
4869
|
var LINK_COMMAND = "hermeslink";
|
|
4507
4870
|
var LINK_DEFAULT_PORT = 52379;
|
|
4508
4871
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4719,6 +5082,27 @@ function readRecentGatewayLogEntries(options = {}) {
|
|
|
4719
5082
|
filePaths: options.filePaths ?? getGatewayLogFiles(paths)
|
|
4720
5083
|
});
|
|
4721
5084
|
}
|
|
5085
|
+
async function flushLogFiles(options) {
|
|
5086
|
+
const maxFiles = Math.max(0, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
5087
|
+
const truncated = [];
|
|
5088
|
+
const removed = [];
|
|
5089
|
+
const filePaths = Array.from(new Set(options.filePaths.map((filePath) => path6.resolve(filePath))));
|
|
5090
|
+
for (const filePath of filePaths) {
|
|
5091
|
+
if (await fileExists(filePath)) {
|
|
5092
|
+
await truncate(filePath, 0);
|
|
5093
|
+
truncated.push(filePath);
|
|
5094
|
+
}
|
|
5095
|
+
for (let index = 1; index <= maxFiles; index += 1) {
|
|
5096
|
+
const rotated = rotatedLogFile(filePath, index);
|
|
5097
|
+
if (!await fileExists(rotated)) {
|
|
5098
|
+
continue;
|
|
5099
|
+
}
|
|
5100
|
+
await rm2(rotated, { force: true });
|
|
5101
|
+
removed.push(rotated);
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
return { truncated, removed };
|
|
5105
|
+
}
|
|
4722
5106
|
function clampLimit(value) {
|
|
4723
5107
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4724
5108
|
return DEFAULT_READ_LIMIT;
|
|
@@ -4871,6 +5255,10 @@ async function moveIfExists(from, to) {
|
|
|
4871
5255
|
}
|
|
4872
5256
|
});
|
|
4873
5257
|
}
|
|
5258
|
+
async function fileExists(filePath) {
|
|
5259
|
+
const info = await stat3(filePath).catch(() => null);
|
|
5260
|
+
return Boolean(info?.isFile());
|
|
5261
|
+
}
|
|
4874
5262
|
function rotatedLogFile(filePath, index) {
|
|
4875
5263
|
return `${filePath}.${index}`;
|
|
4876
5264
|
}
|
|
@@ -5415,15 +5803,15 @@ ${stderr}`.trim();
|
|
|
5415
5803
|
});
|
|
5416
5804
|
});
|
|
5417
5805
|
}
|
|
5418
|
-
function assertHermesRunsApiSupported(version, status) {
|
|
5806
|
+
function assertHermesRunsApiSupported(version, status, endpoint = "/v1/runs") {
|
|
5419
5807
|
if (status !== 404) {
|
|
5420
5808
|
return;
|
|
5421
5809
|
}
|
|
5422
5810
|
const versionText = version?.version ? `\u5F53\u524D\u68C0\u6D4B\u5230 Hermes Agent ${version.version}\u3002` : "";
|
|
5423
5811
|
throw new LinkHttpError(
|
|
5424
5812
|
502,
|
|
5425
|
-
"hermes_runs_api_unsupported",
|
|
5426
|
-
`${versionText}\u5F53\u524D Hermes Agent API Server \u4E0D\u652F\u6301 HermesPilot \u9700\u8981\u7684
|
|
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 } : {},
|
|
@@ -13353,6 +13863,17 @@ async function buildConversationHistory(input) {
|
|
|
13353
13863
|
)
|
|
13354
13864
|
};
|
|
13355
13865
|
}
|
|
13866
|
+
if (snapshotHistory.length > 0) {
|
|
13867
|
+
return {
|
|
13868
|
+
messages: snapshotHistory,
|
|
13869
|
+
source: "link_snapshot",
|
|
13870
|
+
diagnostics: createHistoryDiagnostics(
|
|
13871
|
+
snapshotHistory,
|
|
13872
|
+
"link_snapshot_authoritative",
|
|
13873
|
+
{ snapshot_message_count: snapshotHistory.length }
|
|
13874
|
+
)
|
|
13875
|
+
};
|
|
13876
|
+
}
|
|
13356
13877
|
const hermesHistory = await readHermesTranscriptHistory(
|
|
13357
13878
|
input.hermesSessionId,
|
|
13358
13879
|
input.profileName
|
|
@@ -13396,11 +13917,16 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
|
|
|
13396
13917
|
}));
|
|
13397
13918
|
const [dbHistory, jsonlHistory] = await Promise.all([
|
|
13398
13919
|
readHermesStateDbHistory(dbPath, normalizedSessionId),
|
|
13399
|
-
|
|
13920
|
+
readHermesTranscriptFilesHistory(
|
|
13921
|
+
sessionsDirConfig.sessionsDir,
|
|
13922
|
+
normalizedSessionId
|
|
13923
|
+
)
|
|
13400
13924
|
]);
|
|
13401
13925
|
const diagnosticCounts = {
|
|
13402
13926
|
state_db_message_count: dbHistory.length,
|
|
13403
|
-
|
|
13927
|
+
transcript_message_count: jsonlHistory.messages.length,
|
|
13928
|
+
session_json_message_count: jsonlHistory.sessionJsonMessageCount,
|
|
13929
|
+
jsonl_message_count: jsonlHistory.jsonlMessageCount,
|
|
13404
13930
|
jsonl_byte_count: jsonlHistory.byteCount,
|
|
13405
13931
|
jsonl_line_count: jsonlHistory.lineCount,
|
|
13406
13932
|
jsonl_skipped_line_count: jsonlHistory.skippedLineCount,
|
|
@@ -13413,7 +13939,7 @@ async function readHermesTranscriptHistory(sessionId, profileName) {
|
|
|
13413
13939
|
source: "hermes_transcript",
|
|
13414
13940
|
diagnostics: createHistoryDiagnostics(
|
|
13415
13941
|
jsonlHistory.messages,
|
|
13416
|
-
"
|
|
13942
|
+
"transcript_has_more_messages",
|
|
13417
13943
|
diagnosticCounts
|
|
13418
13944
|
)
|
|
13419
13945
|
};
|
|
@@ -13448,13 +13974,9 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
13448
13974
|
if (!isValidSessionFileStem(sessionId)) {
|
|
13449
13975
|
return empty;
|
|
13450
13976
|
}
|
|
13451
|
-
const
|
|
13452
|
-
|
|
13453
|
-
|
|
13454
|
-
return "";
|
|
13455
|
-
}
|
|
13456
|
-
throw error;
|
|
13457
|
-
});
|
|
13977
|
+
const raw = await readFirstExistingFile(
|
|
13978
|
+
candidateTranscriptPaths(sessionsDir, sessionId, "jsonl")
|
|
13979
|
+
);
|
|
13458
13980
|
if (!raw.trim()) {
|
|
13459
13981
|
return empty;
|
|
13460
13982
|
}
|
|
@@ -13482,6 +14004,93 @@ async function readHermesJsonlHistory(sessionsDir, sessionId) {
|
|
|
13482
14004
|
skippedLineCount
|
|
13483
14005
|
};
|
|
13484
14006
|
}
|
|
14007
|
+
async function readHermesSessionJsonHistory(sessionsDir, sessionId) {
|
|
14008
|
+
const empty = {
|
|
14009
|
+
messages: [],
|
|
14010
|
+
byteCount: 0,
|
|
14011
|
+
skippedMessageCount: 0
|
|
14012
|
+
};
|
|
14013
|
+
if (!isValidSessionFileStem(sessionId)) {
|
|
14014
|
+
return empty;
|
|
14015
|
+
}
|
|
14016
|
+
const raw = await readFirstExistingFile(
|
|
14017
|
+
candidateTranscriptPaths(sessionsDir, sessionId, "json")
|
|
14018
|
+
);
|
|
14019
|
+
if (!raw.trim()) {
|
|
14020
|
+
return empty;
|
|
14021
|
+
}
|
|
14022
|
+
let payload;
|
|
14023
|
+
try {
|
|
14024
|
+
payload = JSON.parse(raw);
|
|
14025
|
+
} catch {
|
|
14026
|
+
return {
|
|
14027
|
+
messages: [],
|
|
14028
|
+
byteCount: Buffer.byteLength(raw, "utf8"),
|
|
14029
|
+
skippedMessageCount: 1
|
|
14030
|
+
};
|
|
14031
|
+
}
|
|
14032
|
+
const records = Array.isArray(payload) ? payload : isRecord2(payload) && Array.isArray(payload.messages) ? payload.messages : [];
|
|
14033
|
+
const messages = [];
|
|
14034
|
+
let skippedMessageCount = 0;
|
|
14035
|
+
for (const record of records) {
|
|
14036
|
+
if (!isRecord2(record)) {
|
|
14037
|
+
skippedMessageCount += 1;
|
|
14038
|
+
continue;
|
|
14039
|
+
}
|
|
14040
|
+
const message = normalizeHistoryRecord(record);
|
|
14041
|
+
if (message) {
|
|
14042
|
+
messages.push(message);
|
|
14043
|
+
} else {
|
|
14044
|
+
skippedMessageCount += 1;
|
|
14045
|
+
}
|
|
14046
|
+
}
|
|
14047
|
+
return {
|
|
14048
|
+
messages,
|
|
14049
|
+
byteCount: Buffer.byteLength(raw, "utf8"),
|
|
14050
|
+
skippedMessageCount
|
|
14051
|
+
};
|
|
14052
|
+
}
|
|
14053
|
+
async function readHermesTranscriptFilesHistory(sessionsDir, sessionId) {
|
|
14054
|
+
const [jsonHistory, jsonlHistory] = await Promise.all([
|
|
14055
|
+
readHermesSessionJsonHistory(sessionsDir, sessionId),
|
|
14056
|
+
readHermesJsonlHistory(sessionsDir, sessionId)
|
|
14057
|
+
]);
|
|
14058
|
+
if (jsonHistory.messages.length > jsonlHistory.messages.length) {
|
|
14059
|
+
return {
|
|
14060
|
+
messages: jsonHistory.messages,
|
|
14061
|
+
byteCount: jsonHistory.byteCount,
|
|
14062
|
+
lineCount: 0,
|
|
14063
|
+
skippedLineCount: jsonHistory.skippedMessageCount,
|
|
14064
|
+
sessionJsonMessageCount: jsonHistory.messages.length,
|
|
14065
|
+
jsonlMessageCount: jsonlHistory.messages.length
|
|
14066
|
+
};
|
|
14067
|
+
}
|
|
14068
|
+
return {
|
|
14069
|
+
...jsonlHistory,
|
|
14070
|
+
sessionJsonMessageCount: jsonHistory.messages.length,
|
|
14071
|
+
jsonlMessageCount: jsonlHistory.messages.length
|
|
14072
|
+
};
|
|
14073
|
+
}
|
|
14074
|
+
async function readFirstExistingFile(paths) {
|
|
14075
|
+
for (const filePath of paths) {
|
|
14076
|
+
const raw = await readFile10(filePath, "utf8").catch((error) => {
|
|
14077
|
+
if (isNodeError12(error, "ENOENT")) {
|
|
14078
|
+
return null;
|
|
14079
|
+
}
|
|
14080
|
+
throw error;
|
|
14081
|
+
});
|
|
14082
|
+
if (raw !== null) {
|
|
14083
|
+
return raw;
|
|
14084
|
+
}
|
|
14085
|
+
}
|
|
14086
|
+
return "";
|
|
14087
|
+
}
|
|
14088
|
+
function candidateTranscriptPaths(sessionsDir, sessionId, extension) {
|
|
14089
|
+
return [
|
|
14090
|
+
path17.join(sessionsDir, `session_${sessionId}.${extension}`),
|
|
14091
|
+
path17.join(sessionsDir, `${sessionId}.${extension}`)
|
|
14092
|
+
];
|
|
14093
|
+
}
|
|
13485
14094
|
function readHistoryRows(dbPath, sessionId) {
|
|
13486
14095
|
let db = null;
|
|
13487
14096
|
try {
|
|
@@ -13534,6 +14143,8 @@ function createHistoryDiagnostics(messages, sourceReason, counts = {}) {
|
|
|
13534
14143
|
return {
|
|
13535
14144
|
source_reason: sourceReason,
|
|
13536
14145
|
state_db_message_count: counts.state_db_message_count ?? 0,
|
|
14146
|
+
transcript_message_count: counts.transcript_message_count ?? 0,
|
|
14147
|
+
session_json_message_count: counts.session_json_message_count ?? 0,
|
|
13537
14148
|
jsonl_message_count: counts.jsonl_message_count ?? 0,
|
|
13538
14149
|
snapshot_message_count: counts.snapshot_message_count ?? 0,
|
|
13539
14150
|
selected_message_count: messages.length,
|
|
@@ -13550,6 +14161,8 @@ function createHistoryDiagnostics(messages, sourceReason, counts = {}) {
|
|
|
13550
14161
|
function pickHermesDiagnosticCounts(diagnostics) {
|
|
13551
14162
|
return {
|
|
13552
14163
|
state_db_message_count: diagnostics.state_db_message_count,
|
|
14164
|
+
transcript_message_count: diagnostics.transcript_message_count,
|
|
14165
|
+
session_json_message_count: diagnostics.session_json_message_count,
|
|
13553
14166
|
jsonl_message_count: diagnostics.jsonl_message_count,
|
|
13554
14167
|
jsonl_byte_count: diagnostics.jsonl_byte_count,
|
|
13555
14168
|
jsonl_line_count: diagnostics.jsonl_line_count,
|
|
@@ -13558,6 +14171,9 @@ function pickHermesDiagnosticCounts(diagnostics) {
|
|
|
13558
14171
|
jsonl_sessions_dir_config_error: diagnostics.jsonl_sessions_dir_config_error
|
|
13559
14172
|
};
|
|
13560
14173
|
}
|
|
14174
|
+
function isRecord2(value) {
|
|
14175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14176
|
+
}
|
|
13561
14177
|
function countReplayMetadata(messages) {
|
|
13562
14178
|
let toolMessageCount = 0;
|
|
13563
14179
|
let toolCallMessageCount = 0;
|
|
@@ -13972,24 +14588,29 @@ async function* parseSseResponse(response) {
|
|
|
13972
14588
|
let buffer = "";
|
|
13973
14589
|
for await (const chunk of response.body) {
|
|
13974
14590
|
buffer += decoder.decode(chunk, { stream: true });
|
|
13975
|
-
let
|
|
13976
|
-
while (
|
|
13977
|
-
const block = buffer.slice(0,
|
|
13978
|
-
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);
|
|
13979
14595
|
const parsed = parseSseBlock(block);
|
|
13980
14596
|
if (parsed) {
|
|
13981
14597
|
yield parsed;
|
|
13982
14598
|
}
|
|
13983
|
-
|
|
14599
|
+
separator = findSseBlockSeparator(buffer);
|
|
13984
14600
|
}
|
|
13985
14601
|
}
|
|
14602
|
+
buffer += decoder.decode();
|
|
13986
14603
|
const trailing = parseSseBlock(buffer);
|
|
13987
14604
|
if (trailing) {
|
|
13988
14605
|
yield trailing;
|
|
13989
14606
|
}
|
|
13990
14607
|
}
|
|
14608
|
+
function findSseBlockSeparator(value) {
|
|
14609
|
+
const match = /\r\n\r\n|\n\n|\r\r/u.exec(value);
|
|
14610
|
+
return match ? { index: match.index, length: match[0].length } : null;
|
|
14611
|
+
}
|
|
13991
14612
|
function parseSseBlock(block) {
|
|
13992
|
-
const lines = block.split(
|
|
14613
|
+
const lines = block.split(/\r\n|\n|\r/u);
|
|
13993
14614
|
let eventName = "";
|
|
13994
14615
|
const data = [];
|
|
13995
14616
|
for (const rawLine of lines) {
|
|
@@ -14049,7 +14670,7 @@ function resolveConversationRunBackend(env = process.env) {
|
|
|
14049
14670
|
if (RUNS_BACKEND_VALUES.has(raw)) {
|
|
14050
14671
|
return "runs";
|
|
14051
14672
|
}
|
|
14052
|
-
return "
|
|
14673
|
+
return "responses";
|
|
14053
14674
|
}
|
|
14054
14675
|
function isRunToolResultCompensationEnabled(env = process.env) {
|
|
14055
14676
|
const raw = env.HERMESLINK_RUN_TOOL_RESULT_COMPENSATION?.trim().toLowerCase();
|
|
@@ -15052,7 +15673,14 @@ var ConversationRunLifecycle = class {
|
|
|
15052
15673
|
runId,
|
|
15053
15674
|
hermesSessionId
|
|
15054
15675
|
);
|
|
15055
|
-
const
|
|
15676
|
+
const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
|
|
15677
|
+
if (previousResponseId) {
|
|
15678
|
+
await this.updateRun(conversationId, runId, {
|
|
15679
|
+
previous_response_id: previousResponseId
|
|
15680
|
+
});
|
|
15681
|
+
}
|
|
15682
|
+
const shouldBuildConversationHistory = backend === "runs" || !previousResponseId;
|
|
15683
|
+
let conversationHistory = shouldBuildConversationHistory ? await buildConversationHistory({
|
|
15056
15684
|
paths: this.deps.paths,
|
|
15057
15685
|
profileName: run.profile,
|
|
15058
15686
|
hermesSessionId,
|
|
@@ -15069,14 +15697,22 @@ var ConversationRunLifecycle = class {
|
|
|
15069
15697
|
source: "empty",
|
|
15070
15698
|
diagnostics: emptyConversationHistoryDiagnostics("build_failed")
|
|
15071
15699
|
};
|
|
15072
|
-
})
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15700
|
+
}) : {
|
|
15701
|
+
messages: [],
|
|
15702
|
+
source: "empty",
|
|
15703
|
+
diagnostics: emptyConversationHistoryDiagnostics("no_history")
|
|
15704
|
+
};
|
|
15705
|
+
await this.deps.logger.debug(
|
|
15706
|
+
shouldBuildConversationHistory ? "conversation_history_built" : "conversation_history_skipped_previous_response",
|
|
15707
|
+
{
|
|
15708
|
+
conversation_id: conversationId,
|
|
15709
|
+
run_id: runId,
|
|
15710
|
+
backend,
|
|
15711
|
+
source: conversationHistory.source,
|
|
15712
|
+
message_count: conversationHistory.messages.length,
|
|
15713
|
+
...conversationHistory.diagnostics
|
|
15714
|
+
}
|
|
15715
|
+
);
|
|
15080
15716
|
const cronJobIdsBeforeRun = await this.readHermesCronJobIds(
|
|
15081
15717
|
run.profile
|
|
15082
15718
|
).catch(() => null);
|
|
@@ -15086,12 +15722,6 @@ var ConversationRunLifecycle = class {
|
|
|
15086
15722
|
fallbackInput: input,
|
|
15087
15723
|
snapshot
|
|
15088
15724
|
});
|
|
15089
|
-
const previousResponseId = backend === "responses" ? findPreviousHermesResponseId(snapshot, run) : void 0;
|
|
15090
|
-
if (previousResponseId) {
|
|
15091
|
-
await this.updateRun(conversationId, runId, {
|
|
15092
|
-
previous_response_id: previousResponseId
|
|
15093
|
-
});
|
|
15094
|
-
}
|
|
15095
15725
|
const deliveryStagingDir = await prepareDeliveryStagingRunDir(
|
|
15096
15726
|
this.deps.paths,
|
|
15097
15727
|
conversationId,
|
|
@@ -15105,12 +15735,12 @@ var ConversationRunLifecycle = class {
|
|
|
15105
15735
|
return void 0;
|
|
15106
15736
|
});
|
|
15107
15737
|
const instructions = buildRunInstructions(run, deliveryStagingDir);
|
|
15108
|
-
const estimatedUsage = estimateContextUsage({
|
|
15738
|
+
const estimatedUsage = shouldBuildConversationHistory ? estimateContextUsage({
|
|
15109
15739
|
conversationHistory: conversationHistory.messages,
|
|
15110
15740
|
currentInput: resolvedInput,
|
|
15111
15741
|
instructions,
|
|
15112
15742
|
contextWindow: run.context_window
|
|
15113
|
-
});
|
|
15743
|
+
}) : void 0;
|
|
15114
15744
|
if (estimatedUsage) {
|
|
15115
15745
|
await this.updateRun(conversationId, runId, { usage: estimatedUsage });
|
|
15116
15746
|
}
|
|
@@ -15119,22 +15749,89 @@ var ConversationRunLifecycle = class {
|
|
|
15119
15749
|
run.profile ?? "default"
|
|
15120
15750
|
);
|
|
15121
15751
|
if (backend === "responses") {
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15752
|
+
let response;
|
|
15753
|
+
try {
|
|
15754
|
+
response = await streamHermesResponses(
|
|
15755
|
+
{
|
|
15756
|
+
input: resolvedInput,
|
|
15757
|
+
instructions,
|
|
15758
|
+
session_id: hermesSessionId,
|
|
15759
|
+
session_key: sessionKey,
|
|
15760
|
+
model: run.model,
|
|
15761
|
+
...previousResponseId ? { previous_response_id: previousResponseId } : {},
|
|
15762
|
+
...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
|
|
15763
|
+
},
|
|
15764
|
+
{
|
|
15765
|
+
logger: this.deps.logger,
|
|
15766
|
+
profileName: run.profile,
|
|
15767
|
+
signal: controller.signal
|
|
15768
|
+
}
|
|
15769
|
+
);
|
|
15770
|
+
} catch (error) {
|
|
15771
|
+
if (!previousResponseId || !isLinkHttpError(error) || error.code !== "hermes_previous_response_not_found") {
|
|
15772
|
+
throw error;
|
|
15136
15773
|
}
|
|
15137
|
-
|
|
15774
|
+
await this.deps.logger.warn(
|
|
15775
|
+
"hermes_previous_response_missing_falling_back_to_history",
|
|
15776
|
+
{
|
|
15777
|
+
conversation_id: conversationId,
|
|
15778
|
+
run_id: runId,
|
|
15779
|
+
previous_response_id: previousResponseId,
|
|
15780
|
+
error: error.message
|
|
15781
|
+
}
|
|
15782
|
+
);
|
|
15783
|
+
await this.updateRun(conversationId, runId, {
|
|
15784
|
+
previous_response_id: void 0
|
|
15785
|
+
});
|
|
15786
|
+
conversationHistory = await buildConversationHistory({
|
|
15787
|
+
paths: this.deps.paths,
|
|
15788
|
+
profileName: run.profile,
|
|
15789
|
+
hermesSessionId,
|
|
15790
|
+
snapshot,
|
|
15791
|
+
run
|
|
15792
|
+
}).catch(async (buildError) => {
|
|
15793
|
+
await this.deps.logger.warn("conversation_history_build_failed", {
|
|
15794
|
+
conversation_id: conversationId,
|
|
15795
|
+
run_id: runId,
|
|
15796
|
+
error: buildError instanceof Error ? buildError.message : String(buildError)
|
|
15797
|
+
});
|
|
15798
|
+
return {
|
|
15799
|
+
messages: [],
|
|
15800
|
+
source: "empty",
|
|
15801
|
+
diagnostics: emptyConversationHistoryDiagnostics("build_failed")
|
|
15802
|
+
};
|
|
15803
|
+
});
|
|
15804
|
+
await this.deps.logger.debug("conversation_history_built", {
|
|
15805
|
+
conversation_id: conversationId,
|
|
15806
|
+
run_id: runId,
|
|
15807
|
+
backend,
|
|
15808
|
+
source: conversationHistory.source,
|
|
15809
|
+
message_count: conversationHistory.messages.length,
|
|
15810
|
+
...conversationHistory.diagnostics
|
|
15811
|
+
});
|
|
15812
|
+
const fallbackUsage = estimateContextUsage({
|
|
15813
|
+
conversationHistory: conversationHistory.messages,
|
|
15814
|
+
currentInput: resolvedInput,
|
|
15815
|
+
instructions,
|
|
15816
|
+
contextWindow: run.context_window
|
|
15817
|
+
});
|
|
15818
|
+
await this.updateRun(conversationId, runId, { usage: fallbackUsage });
|
|
15819
|
+
response = await streamHermesResponses(
|
|
15820
|
+
{
|
|
15821
|
+
input: resolvedInput,
|
|
15822
|
+
instructions,
|
|
15823
|
+
session_id: hermesSessionId,
|
|
15824
|
+
session_key: sessionKey,
|
|
15825
|
+
model: run.model,
|
|
15826
|
+
...conversationHistory.messages.length > 0 ? { conversation_history: conversationHistory.messages } : {}
|
|
15827
|
+
},
|
|
15828
|
+
{
|
|
15829
|
+
logger: this.deps.logger,
|
|
15830
|
+
profileName: run.profile,
|
|
15831
|
+
signal: controller.signal
|
|
15832
|
+
}
|
|
15833
|
+
);
|
|
15834
|
+
}
|
|
15138
15835
|
const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
|
|
15139
15836
|
if (responseSessionId) {
|
|
15140
15837
|
await this.rememberRunHermesSessionId(
|
|
@@ -15230,6 +15927,7 @@ var ConversationRunLifecycle = class {
|
|
|
15230
15927
|
);
|
|
15231
15928
|
}
|
|
15232
15929
|
async failRun(conversationId, runId, message, source) {
|
|
15930
|
+
await this.refreshRunHermesCompressionTip(conversationId, runId);
|
|
15233
15931
|
return this.deps.withConversationLock(
|
|
15234
15932
|
conversationId,
|
|
15235
15933
|
() => this.failRunLocked(conversationId, runId, message, source)
|
|
@@ -15327,9 +16025,21 @@ var ConversationRunLifecycle = class {
|
|
|
15327
16025
|
await this.cancelRunAfterAbort(input.conversationId, input.runId);
|
|
15328
16026
|
return;
|
|
15329
16027
|
}
|
|
15330
|
-
|
|
16028
|
+
const hasAssistantOutput = await this.runHasAssistantOutput(
|
|
16029
|
+
input.conversationId,
|
|
16030
|
+
input.runId
|
|
16031
|
+
);
|
|
16032
|
+
if (input.backend === "responses" && !streamError && hasAssistantOutput) {
|
|
15331
16033
|
await this.completeRun(input.conversationId, input.runId);
|
|
15332
16034
|
} else {
|
|
16035
|
+
await this.deps.logger.warn("hermes_event_stream_ended_without_terminal", {
|
|
16036
|
+
backend: input.backend,
|
|
16037
|
+
conversation_id: input.conversationId,
|
|
16038
|
+
run_id: input.runId,
|
|
16039
|
+
...input.hermesRunId ? { hermes_run_id: input.hermesRunId } : {},
|
|
16040
|
+
has_assistant_output: hasAssistantOutput,
|
|
16041
|
+
...streamError ? { error: formatUnknownErrorMessage(streamError) } : {}
|
|
16042
|
+
});
|
|
15333
16043
|
await this.failRun(
|
|
15334
16044
|
input.conversationId,
|
|
15335
16045
|
input.runId,
|
|
@@ -15947,11 +16657,38 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
15947
16657
|
return user ? messageRequestsAppDelivery(messageText(user)) : false;
|
|
15948
16658
|
}
|
|
15949
16659
|
async completeRun(conversationId, runId, source) {
|
|
16660
|
+
await this.refreshRunHermesCompressionTip(conversationId, runId);
|
|
15950
16661
|
return this.deps.withConversationLock(
|
|
15951
16662
|
conversationId,
|
|
15952
16663
|
() => this.completeRunLocked(conversationId, runId, source)
|
|
15953
16664
|
);
|
|
15954
16665
|
}
|
|
16666
|
+
async refreshRunHermesCompressionTip(conversationId, runId) {
|
|
16667
|
+
const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
|
|
16668
|
+
const run = snapshot?.runs.find((item) => item.id === runId);
|
|
16669
|
+
if (!run?.hermes_session_id) {
|
|
16670
|
+
return;
|
|
16671
|
+
}
|
|
16672
|
+
const compressionTip = await readHermesCompressionTip(
|
|
16673
|
+
run.hermes_session_id,
|
|
16674
|
+
this.deps.paths,
|
|
16675
|
+
run.profile
|
|
16676
|
+
).catch(() => void 0);
|
|
16677
|
+
if (!compressionTip || compressionTip === run.hermes_session_id) {
|
|
16678
|
+
return;
|
|
16679
|
+
}
|
|
16680
|
+
await this.deps.logger.info("hermes_compression_tip_detected", {
|
|
16681
|
+
conversation_id: conversationId,
|
|
16682
|
+
run_id: runId,
|
|
16683
|
+
previous_hermes_session_id: run.hermes_session_id,
|
|
16684
|
+
hermes_session_id: compressionTip
|
|
16685
|
+
});
|
|
16686
|
+
await this.rememberRunHermesSessionId(
|
|
16687
|
+
conversationId,
|
|
16688
|
+
runId,
|
|
16689
|
+
compressionTip
|
|
16690
|
+
);
|
|
16691
|
+
}
|
|
15955
16692
|
async completeRunLocked(conversationId, runId, source) {
|
|
15956
16693
|
let snapshot = await this.deps.readSnapshot(conversationId);
|
|
15957
16694
|
let run = snapshot.runs.find((item) => item.id === runId);
|
|
@@ -16638,6 +17375,7 @@ var ConversationService = class {
|
|
|
16638
17375
|
metadata: this.metadata,
|
|
16639
17376
|
commandHandlers: this.commandHandlers,
|
|
16640
17377
|
runLifecycle: this.runLifecycle,
|
|
17378
|
+
logger: this.logger,
|
|
16641
17379
|
withConversationLock: (conversationId, task) => this.withConversationLock(conversationId, task),
|
|
16642
17380
|
appendEvent: (conversationId, input) => this.appendEvent(conversationId, input),
|
|
16643
17381
|
resolveMessageAttachmentParts: (conversationId, attachments) => this.maintenance.resolveMessageAttachmentParts(
|
|
@@ -17156,7 +17894,14 @@ var ConversationService = class {
|
|
|
17156
17894
|
reason: "cancelled by app"
|
|
17157
17895
|
});
|
|
17158
17896
|
if (result.run.status === "cancelled") {
|
|
17159
|
-
void this.orchestration.startNextQueuedRun(conversationId)
|
|
17897
|
+
void this.orchestration.startNextQueuedRun(conversationId).catch(
|
|
17898
|
+
(error) => {
|
|
17899
|
+
void this.logger.warn("conversation_queue_drain_failed", {
|
|
17900
|
+
conversation_id: conversationId,
|
|
17901
|
+
error: error instanceof Error ? error.message : String(error)
|
|
17902
|
+
});
|
|
17903
|
+
}
|
|
17904
|
+
);
|
|
17160
17905
|
}
|
|
17161
17906
|
return result;
|
|
17162
17907
|
}
|
|
@@ -18176,6 +18921,7 @@ function isLanHost(hostname) {
|
|
|
18176
18921
|
// src/http/sse.ts
|
|
18177
18922
|
var DEFAULT_SSE_RETRY_MS = 1e3;
|
|
18178
18923
|
var DEFAULT_SSE_HEARTBEAT_MS = 15e3;
|
|
18924
|
+
var activeSseSockets = /* @__PURE__ */ new WeakSet();
|
|
18179
18925
|
function beginSseStream(request, response, options = {}) {
|
|
18180
18926
|
const retryMs = normalizeRetryMs(options.retryMs);
|
|
18181
18927
|
const heartbeatMs = Math.max(1e3, options.heartbeatMs ?? DEFAULT_SSE_HEARTBEAT_MS);
|
|
@@ -18183,11 +18929,15 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18183
18929
|
response.setHeader("content-type", "text/event-stream; charset=utf-8");
|
|
18184
18930
|
response.setHeader("cache-control", "no-store");
|
|
18185
18931
|
response.setHeader("connection", "keep-alive");
|
|
18932
|
+
activeSseSockets.add(request.socket);
|
|
18186
18933
|
response.flushHeaders();
|
|
18187
18934
|
writeSseRetry(response, retryMs);
|
|
18188
18935
|
writeSseComment(response, options.initialComment ?? "connected");
|
|
18189
18936
|
let closed = false;
|
|
18190
18937
|
let heartbeat = null;
|
|
18938
|
+
const onStreamError = () => {
|
|
18939
|
+
cleanup();
|
|
18940
|
+
};
|
|
18191
18941
|
const cleanup = () => {
|
|
18192
18942
|
if (closed) {
|
|
18193
18943
|
return;
|
|
@@ -18199,9 +18949,18 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18199
18949
|
}
|
|
18200
18950
|
request.off("close", cleanup);
|
|
18201
18951
|
response.off("close", cleanup);
|
|
18952
|
+
request.off("error", onStreamError);
|
|
18953
|
+
response.off("error", onStreamError);
|
|
18954
|
+
activeSseSockets.delete(request.socket);
|
|
18202
18955
|
options.onClose?.();
|
|
18203
18956
|
if (!response.writableEnded && !response.destroyed) {
|
|
18204
|
-
|
|
18957
|
+
try {
|
|
18958
|
+
response.end();
|
|
18959
|
+
} catch (error) {
|
|
18960
|
+
if (!isExpectedClientDisconnectError(error)) {
|
|
18961
|
+
throw error;
|
|
18962
|
+
}
|
|
18963
|
+
}
|
|
18205
18964
|
}
|
|
18206
18965
|
};
|
|
18207
18966
|
heartbeat = setInterval(() => {
|
|
@@ -18214,37 +18973,43 @@ function beginSseStream(request, response, options = {}) {
|
|
|
18214
18973
|
heartbeat.unref();
|
|
18215
18974
|
request.once("close", cleanup);
|
|
18216
18975
|
response.once("close", cleanup);
|
|
18976
|
+
request.once("error", onStreamError);
|
|
18977
|
+
response.once("error", onStreamError);
|
|
18217
18978
|
return cleanup;
|
|
18218
18979
|
}
|
|
18980
|
+
function isActiveSseSocket(socket) {
|
|
18981
|
+
return socket != null && activeSseSockets.has(socket);
|
|
18982
|
+
}
|
|
18219
18983
|
function writeSseEvent(response, event) {
|
|
18984
|
+
const appEvent = projectAppConversationEvent(event);
|
|
18220
18985
|
writeJsonSseEvent(response, {
|
|
18221
|
-
event:
|
|
18222
|
-
data:
|
|
18223
|
-
id:
|
|
18986
|
+
event: appEvent.type,
|
|
18987
|
+
data: appEvent,
|
|
18988
|
+
id: appEvent.seq
|
|
18224
18989
|
});
|
|
18225
18990
|
}
|
|
18226
18991
|
function writeJsonSseEvent(response, event) {
|
|
18227
18992
|
if (event.retryMs != null) {
|
|
18228
|
-
response
|
|
18993
|
+
writeResponse(response, `retry: ${normalizeRetryMs(event.retryMs)}
|
|
18229
18994
|
`);
|
|
18230
18995
|
}
|
|
18231
18996
|
if (event.id != null && event.id !== "") {
|
|
18232
|
-
response
|
|
18997
|
+
writeResponse(response, `id: ${event.id}
|
|
18233
18998
|
`);
|
|
18234
18999
|
}
|
|
18235
|
-
response
|
|
19000
|
+
writeResponse(response, `event: ${event.event}
|
|
18236
19001
|
`);
|
|
18237
|
-
response
|
|
19002
|
+
writeResponse(response, `data: ${JSON.stringify(event.data)}
|
|
18238
19003
|
|
|
18239
19004
|
`);
|
|
18240
19005
|
}
|
|
18241
19006
|
function writeSseComment(response, comment = "keep-alive") {
|
|
18242
|
-
response
|
|
19007
|
+
writeResponse(response, `: ${comment}
|
|
18243
19008
|
|
|
18244
19009
|
`);
|
|
18245
19010
|
}
|
|
18246
19011
|
function writeSseRetry(response, retryMs) {
|
|
18247
|
-
response
|
|
19012
|
+
writeResponse(response, `retry: ${normalizeRetryMs(retryMs)}
|
|
18248
19013
|
|
|
18249
19014
|
`);
|
|
18250
19015
|
}
|
|
@@ -18252,6 +19017,25 @@ function normalizeRetryMs(retryMs) {
|
|
|
18252
19017
|
const parsed = Number.isFinite(retryMs) ? Math.trunc(retryMs) : DEFAULT_SSE_RETRY_MS;
|
|
18253
19018
|
return parsed >= 0 ? parsed : DEFAULT_SSE_RETRY_MS;
|
|
18254
19019
|
}
|
|
19020
|
+
function writeResponse(response, chunk) {
|
|
19021
|
+
if (response.writableEnded || response.destroyed) {
|
|
19022
|
+
return;
|
|
19023
|
+
}
|
|
19024
|
+
try {
|
|
19025
|
+
response.write(chunk);
|
|
19026
|
+
} catch (error) {
|
|
19027
|
+
if (!isExpectedClientDisconnectError(error)) {
|
|
19028
|
+
throw error;
|
|
19029
|
+
}
|
|
19030
|
+
}
|
|
19031
|
+
}
|
|
19032
|
+
function isExpectedClientDisconnectError(error) {
|
|
19033
|
+
if (!(error instanceof Error)) {
|
|
19034
|
+
return false;
|
|
19035
|
+
}
|
|
19036
|
+
const code = String(error.code ?? "");
|
|
19037
|
+
return code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || code === "ETIMEDOUT" || /(?:socket hang up|aborted|write after end)/iu.test(error.message);
|
|
19038
|
+
}
|
|
18255
19039
|
|
|
18256
19040
|
// src/http/routes/conversations.ts
|
|
18257
19041
|
function registerConversationRoutes(router, options) {
|
|
@@ -18732,12 +19516,22 @@ function encodeRfc5987Value(value) {
|
|
|
18732
19516
|
}
|
|
18733
19517
|
|
|
18734
19518
|
// src/http/middleware/error-handler.ts
|
|
19519
|
+
var INTERNAL_HEALTH_PROBE_HEADER = "x-hermes-link-internal-health-probe";
|
|
18735
19520
|
function createHttpErrorMiddleware(logger) {
|
|
18736
19521
|
return async (ctx, next) => {
|
|
18737
19522
|
const startedAt = Date.now();
|
|
19523
|
+
const shouldSkipRequestLog = isInternalHealthProbe(ctx);
|
|
19524
|
+
let expectedClientDisconnect = false;
|
|
18738
19525
|
try {
|
|
18739
19526
|
await next();
|
|
18740
19527
|
} catch (error) {
|
|
19528
|
+
if (isExpectedClientDisconnectError2(error, {
|
|
19529
|
+
sse: isSseRequestContext(ctx)
|
|
19530
|
+
})) {
|
|
19531
|
+
expectedClientDisconnect = true;
|
|
19532
|
+
ctx.respond = false;
|
|
19533
|
+
return;
|
|
19534
|
+
}
|
|
18741
19535
|
const profileError = error instanceof Error && error.message === "invalid profile name";
|
|
18742
19536
|
const profileNotFound = error instanceof Error && error.message === "profile does not exist";
|
|
18743
19537
|
const status = isLinkHttpError(error) ? error.status : profileError ? 400 : profileNotFound ? 404 : 500;
|
|
@@ -18750,28 +19544,57 @@ function createHttpErrorMiddleware(logger) {
|
|
|
18750
19544
|
message: error instanceof Error ? error.message : "Internal error"
|
|
18751
19545
|
}
|
|
18752
19546
|
};
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
19547
|
+
if (!shouldSkipRequestLog) {
|
|
19548
|
+
void logger.write(
|
|
19549
|
+
status >= 500 ? "error" : "warn",
|
|
19550
|
+
"http_request_failed",
|
|
19551
|
+
{
|
|
19552
|
+
method: ctx.method,
|
|
19553
|
+
path: ctx.path,
|
|
19554
|
+
query: ctx.querystring || null,
|
|
19555
|
+
status,
|
|
19556
|
+
code,
|
|
19557
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19558
|
+
}
|
|
19559
|
+
);
|
|
19560
|
+
}
|
|
19561
|
+
} finally {
|
|
19562
|
+
if (!shouldSkipRequestLog && !expectedClientDisconnect) {
|
|
19563
|
+
void logger.info("http_request", {
|
|
18757
19564
|
method: ctx.method,
|
|
18758
19565
|
path: ctx.path,
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
}
|
|
18764
|
-
);
|
|
18765
|
-
} finally {
|
|
18766
|
-
void logger.info("http_request", {
|
|
18767
|
-
method: ctx.method,
|
|
18768
|
-
path: ctx.path,
|
|
18769
|
-
status: ctx.status,
|
|
18770
|
-
duration_ms: Date.now() - startedAt
|
|
18771
|
-
});
|
|
19566
|
+
status: ctx.status,
|
|
19567
|
+
duration_ms: Date.now() - startedAt
|
|
19568
|
+
});
|
|
19569
|
+
}
|
|
18772
19570
|
}
|
|
18773
19571
|
};
|
|
18774
19572
|
}
|
|
19573
|
+
function isInternalHealthProbe(ctx) {
|
|
19574
|
+
return ctx.path === "/api/v1/bootstrap" && ctx.get(INTERNAL_HEALTH_PROBE_HEADER) === "1";
|
|
19575
|
+
}
|
|
19576
|
+
function isSseRequestContext(ctx) {
|
|
19577
|
+
if (!ctx) {
|
|
19578
|
+
return false;
|
|
19579
|
+
}
|
|
19580
|
+
return isSseRequestPath(ctx.path) || isActiveSseSocket(ctx.req.socket);
|
|
19581
|
+
}
|
|
19582
|
+
function isSseRequestPath(path29) {
|
|
19583
|
+
if (!path29) {
|
|
19584
|
+
return false;
|
|
19585
|
+
}
|
|
19586
|
+
return path29 === "/api/v1/conversations/events" || path29 === "/api/v1/profile-creation/events" || path29 === "/api/v1/hermes/update/events" || path29 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path29) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path29);
|
|
19587
|
+
}
|
|
19588
|
+
function isExpectedClientDisconnectError2(error, options = {}) {
|
|
19589
|
+
if (!(error instanceof Error)) {
|
|
19590
|
+
return false;
|
|
19591
|
+
}
|
|
19592
|
+
const code = String(error.code ?? "");
|
|
19593
|
+
if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted|write after end)/iu.test(error.message)) {
|
|
19594
|
+
return true;
|
|
19595
|
+
}
|
|
19596
|
+
return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
|
|
19597
|
+
}
|
|
18775
19598
|
|
|
18776
19599
|
// src/hermes/profiles.ts
|
|
18777
19600
|
import { execFile as execFile4 } from "child_process";
|
|
@@ -19749,7 +20572,12 @@ function readModelConfigInput(body) {
|
|
|
19749
20572
|
function readModelDefaultsInput(body) {
|
|
19750
20573
|
return {
|
|
19751
20574
|
taskModelId: readString16(body, "task_model_id") ?? readString16(body, "taskModelId") ?? readString16(body, "default_model_id") ?? readString16(body, "defaultModelId") ?? void 0,
|
|
19752
|
-
|
|
20575
|
+
taskModelProvider: readString16(body, "task_model_provider") ?? readString16(body, "taskModelProvider") ?? readString16(body, "default_model_provider") ?? readString16(body, "defaultModelProvider") ?? void 0,
|
|
20576
|
+
taskModelBaseUrl: readString16(body, "task_model_base_url") ?? readString16(body, "taskModelBaseUrl") ?? readString16(body, "default_model_base_url") ?? readString16(body, "defaultModelBaseUrl") ?? void 0,
|
|
20577
|
+
compressionModelId: readString16(body, "compression_model_id") ?? readString16(body, "compressionModelId") ?? void 0,
|
|
20578
|
+
compressionModelProvider: readString16(body, "compression_model_provider") ?? readString16(body, "compressionModelProvider") ?? void 0,
|
|
20579
|
+
compressionModelBaseUrl: readString16(body, "compression_model_base_url") ?? readString16(body, "compressionModelBaseUrl") ?? void 0,
|
|
20580
|
+
reasoningEffort: readString16(body, "reasoning_effort") ?? readString16(body, "reasoningEffort") ?? readString16(body, "default_reasoning_effort") ?? readString16(body, "defaultReasoningEffort") ?? void 0
|
|
19753
20581
|
};
|
|
19754
20582
|
}
|
|
19755
20583
|
function readModelConfigImportInput(body) {
|
|
@@ -19852,41 +20680,348 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
19852
20680
|
import {
|
|
19853
20681
|
cp,
|
|
19854
20682
|
mkdir as mkdir11,
|
|
19855
|
-
readFile as
|
|
20683
|
+
readFile as readFile15,
|
|
19856
20684
|
rm as rm6,
|
|
19857
|
-
stat as
|
|
20685
|
+
stat as stat15
|
|
19858
20686
|
} from "fs/promises";
|
|
20687
|
+
import path22 from "path";
|
|
20688
|
+
import YAML4 from "yaml";
|
|
20689
|
+
|
|
20690
|
+
// src/hermes/link-skill.ts
|
|
20691
|
+
import { readFile as readFile14, stat as stat14 } from "fs/promises";
|
|
20692
|
+
import os4 from "os";
|
|
19859
20693
|
import path21 from "path";
|
|
19860
20694
|
import YAML3 from "yaml";
|
|
19861
|
-
var
|
|
19862
|
-
var
|
|
19863
|
-
var
|
|
19864
|
-
var
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
20695
|
+
var HERMES_LINK_SKILL_ROOT_DIR = "hermes-skills";
|
|
20696
|
+
var HERMES_LINK_SKILL_DIR = "hermes-link";
|
|
20697
|
+
var HERMES_LINK_SKILL_FILE = "SKILL.md";
|
|
20698
|
+
var HERMES_LINK_SKILL_CONTENT = `---
|
|
20699
|
+
name: hermes-link
|
|
20700
|
+
description: Understand and troubleshoot Hermes Link, the local companion service that connects Hermes Agent to the HermesPilot mobile app.
|
|
20701
|
+
---
|
|
20702
|
+
|
|
20703
|
+
# Hermes Link
|
|
20704
|
+
|
|
20705
|
+
Hermes Link is a secure local companion service for hermes-agent. It lets the HermesPilot mobile app connect to the user's local Hermes Agent safely and reliably.
|
|
20706
|
+
|
|
20707
|
+
Hermes Link is specifically for HermesPilot. It is not OpenClaw, ClawPilot, clawlink, or any OpenClaw companion service. Do not confuse those systems when helping the user.
|
|
20708
|
+
|
|
20709
|
+
## What Hermes Link Does
|
|
20710
|
+
|
|
20711
|
+
Hermes Link can:
|
|
20712
|
+
|
|
20713
|
+
- pair the HermesPilot mobile app with this computer
|
|
20714
|
+
- expose a controlled local API for the app
|
|
20715
|
+
- connect through LAN, public direct routes, or Hermes Relay
|
|
20716
|
+
- help the app access Hermes profiles, conversations, messages, files, voice input, logs, and status
|
|
20717
|
+
- start or check the Hermes Gateway when needed
|
|
20718
|
+
|
|
20719
|
+
Hermes Link does not replace Hermes Agent. Hermes Agent remains the actual AI agent runtime.
|
|
20720
|
+
|
|
20721
|
+
## When To Use This Skill
|
|
20722
|
+
|
|
20723
|
+
Use this skill when the user asks about:
|
|
20724
|
+
|
|
20725
|
+
- HermesPilot App cannot connect
|
|
20726
|
+
- Link appears offline
|
|
20727
|
+
- pairing or QR code problems
|
|
20728
|
+
- mobile access to local Hermes
|
|
20729
|
+
- Relay, LAN, or public direct route problems
|
|
20730
|
+
- Hermes Link logs, daemon status, or diagnostics
|
|
20731
|
+
|
|
20732
|
+
## Useful Commands
|
|
20733
|
+
|
|
20734
|
+
Start with:
|
|
20735
|
+
|
|
20736
|
+
\`\`\`bash
|
|
20737
|
+
hermeslink status
|
|
20738
|
+
hermeslink doctor
|
|
20739
|
+
\`\`\`
|
|
20740
|
+
|
|
20741
|
+
For logs:
|
|
20742
|
+
|
|
20743
|
+
\`\`\`bash
|
|
20744
|
+
hermeslink logs --error -n 50
|
|
20745
|
+
hermeslink logs --warn -n 100
|
|
20746
|
+
hermeslink logs -f
|
|
20747
|
+
hermeslink logs --all --level debug -f
|
|
20748
|
+
\`\`\`
|
|
20749
|
+
|
|
20750
|
+
If the daemon appears stuck, suggest:
|
|
20751
|
+
|
|
20752
|
+
\`\`\`bash
|
|
20753
|
+
hermeslink restart
|
|
20754
|
+
\`\`\`
|
|
20755
|
+
|
|
20756
|
+
Explain that restarting Hermes Link may briefly disconnect the mobile app.
|
|
20757
|
+
|
|
20758
|
+
## Troubleshooting Flow
|
|
20759
|
+
|
|
20760
|
+
1. Check \`hermeslink status\`.
|
|
20761
|
+
2. Run \`hermeslink doctor\`.
|
|
20762
|
+
3. Inspect recent errors with \`hermeslink logs --error -n 50\`.
|
|
20763
|
+
4. If the issue is intermittent, reproduce it while running \`hermeslink logs -f\`.
|
|
20764
|
+
5. Check whether the phone and computer are on the same LAN, using Relay, or using a public direct route.
|
|
20765
|
+
6. If the user uses WSL, Docker, or a VM, verify that Hermes Agent and Hermes Link run in the same environment.
|
|
20766
|
+
|
|
20767
|
+
## Safety
|
|
20768
|
+
|
|
20769
|
+
Never reveal API keys, access tokens, refresh tokens, private keys, or full .env contents.
|
|
20770
|
+
|
|
20771
|
+
Do not recommend exposing port 52379 directly to the public internet without TLS, VPN, Tailscale, WireGuard, or another access-control layer.
|
|
20772
|
+
|
|
20773
|
+
Do not modify Hermes profiles, delete user data, edit config files, or kill processes unless the user explicitly asks.
|
|
20774
|
+
`;
|
|
20775
|
+
async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
|
|
20776
|
+
const paths = options.paths ?? resolveRuntimePaths();
|
|
20777
|
+
const externalDir = resolveHermesLinkSkillExternalDir(paths);
|
|
20778
|
+
const skillPath = path21.join(
|
|
20779
|
+
externalDir,
|
|
20780
|
+
HERMES_LINK_SKILL_DIR,
|
|
20781
|
+
HERMES_LINK_SKILL_FILE
|
|
20782
|
+
);
|
|
20783
|
+
const skillChanged = await writeHermesLinkSkill(skillPath);
|
|
20784
|
+
const profiles = await listHermesProfiles(paths);
|
|
20785
|
+
const results = [];
|
|
20786
|
+
for (const profile of profiles) {
|
|
20787
|
+
try {
|
|
20788
|
+
results.push(await ensureProfileUsesExternalSkillDir(profile, externalDir));
|
|
20789
|
+
} catch (error) {
|
|
20790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20791
|
+
results.push({
|
|
20792
|
+
profile: profile.name,
|
|
20793
|
+
profilePath: profile.path,
|
|
20794
|
+
configPath: profile.configPath,
|
|
20795
|
+
changed: false,
|
|
20796
|
+
backupPath: null,
|
|
20797
|
+
skipped: false,
|
|
20798
|
+
error: message
|
|
20799
|
+
});
|
|
20800
|
+
}
|
|
19878
20801
|
}
|
|
19879
|
-
const
|
|
19880
|
-
const
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
19884
|
-
|
|
19885
|
-
|
|
19886
|
-
|
|
19887
|
-
|
|
19888
|
-
|
|
19889
|
-
|
|
20802
|
+
const changedProfiles = results.filter((result) => result.changed);
|
|
20803
|
+
const failedProfiles = results.filter((result) => result.error);
|
|
20804
|
+
if (skillChanged || changedProfiles.length > 0) {
|
|
20805
|
+
void options.logger?.info("hermes_link_skill_ensured", {
|
|
20806
|
+
source: options.source ?? "unspecified",
|
|
20807
|
+
external_dir: externalDir,
|
|
20808
|
+
skill_path: skillPath,
|
|
20809
|
+
skill_changed: skillChanged,
|
|
20810
|
+
changed_profiles: changedProfiles.map((result) => result.profile),
|
|
20811
|
+
failed_profiles: failedProfiles.map((result) => result.profile)
|
|
20812
|
+
});
|
|
20813
|
+
}
|
|
20814
|
+
if (failedProfiles.length > 0) {
|
|
20815
|
+
void options.logger?.warn("hermes_link_skill_profile_ensure_failed", {
|
|
20816
|
+
source: options.source ?? "unspecified",
|
|
20817
|
+
profiles: failedProfiles.map((result) => ({
|
|
20818
|
+
profile: result.profile,
|
|
20819
|
+
error: result.error ?? "unknown error"
|
|
20820
|
+
}))
|
|
20821
|
+
});
|
|
20822
|
+
}
|
|
20823
|
+
return {
|
|
20824
|
+
externalDir,
|
|
20825
|
+
skillPath,
|
|
20826
|
+
skillChanged,
|
|
20827
|
+
profiles: results
|
|
20828
|
+
};
|
|
20829
|
+
}
|
|
20830
|
+
async function ensureHermesLinkSkillInstalledBestEffort(options = {}) {
|
|
20831
|
+
try {
|
|
20832
|
+
await ensureHermesLinkSkillInstalledForProfiles(options);
|
|
20833
|
+
} catch (error) {
|
|
20834
|
+
void options.logger?.warn("hermes_link_skill_ensure_failed", {
|
|
20835
|
+
source: options.source ?? "unspecified",
|
|
20836
|
+
error: error instanceof Error ? error.message : String(error)
|
|
20837
|
+
});
|
|
20838
|
+
}
|
|
20839
|
+
}
|
|
20840
|
+
function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
|
|
20841
|
+
return path21.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
|
|
20842
|
+
}
|
|
20843
|
+
async function writeHermesLinkSkill(skillPath) {
|
|
20844
|
+
const existing = await readFile14(skillPath, "utf8").catch((error) => {
|
|
20845
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20846
|
+
return null;
|
|
20847
|
+
}
|
|
20848
|
+
throw error;
|
|
20849
|
+
});
|
|
20850
|
+
if (existing === HERMES_LINK_SKILL_CONTENT) {
|
|
20851
|
+
return false;
|
|
20852
|
+
}
|
|
20853
|
+
await atomicWriteFilePreservingMetadata(skillPath, HERMES_LINK_SKILL_CONTENT);
|
|
20854
|
+
return true;
|
|
20855
|
+
}
|
|
20856
|
+
async function ensureProfileUsesExternalSkillDir(profile, externalDir) {
|
|
20857
|
+
const profilePath = resolveHermesProfileDir(profile.name);
|
|
20858
|
+
const configPath = resolveHermesConfigPath(profile.name);
|
|
20859
|
+
if (!await pathIsDirectory(profilePath)) {
|
|
20860
|
+
return {
|
|
20861
|
+
profile: profile.name,
|
|
20862
|
+
profilePath,
|
|
20863
|
+
configPath,
|
|
20864
|
+
changed: false,
|
|
20865
|
+
backupPath: null,
|
|
20866
|
+
skipped: true
|
|
20867
|
+
};
|
|
20868
|
+
}
|
|
20869
|
+
const { document, config, existingRaw } = await readHermesConfigDocument2(configPath);
|
|
20870
|
+
const skillsConfig = ensureRecord2(config, "skills");
|
|
20871
|
+
if (externalDirsInclude(skillsConfig.external_dirs, externalDir, profilePath)) {
|
|
20872
|
+
return {
|
|
20873
|
+
profile: profile.name,
|
|
20874
|
+
profilePath,
|
|
20875
|
+
configPath,
|
|
20876
|
+
changed: false,
|
|
20877
|
+
backupPath: null,
|
|
20878
|
+
skipped: false
|
|
20879
|
+
};
|
|
20880
|
+
}
|
|
20881
|
+
skillsConfig.external_dirs = appendExternalDir(
|
|
20882
|
+
skillsConfig.external_dirs,
|
|
20883
|
+
externalDir,
|
|
20884
|
+
profilePath
|
|
20885
|
+
);
|
|
20886
|
+
const backupPath = await writeHermesConfigDocument2({
|
|
20887
|
+
configPath,
|
|
20888
|
+
document,
|
|
20889
|
+
config,
|
|
20890
|
+
existingRaw
|
|
20891
|
+
});
|
|
20892
|
+
return {
|
|
20893
|
+
profile: profile.name,
|
|
20894
|
+
profilePath,
|
|
20895
|
+
configPath,
|
|
20896
|
+
changed: true,
|
|
20897
|
+
backupPath,
|
|
20898
|
+
skipped: false
|
|
20899
|
+
};
|
|
20900
|
+
}
|
|
20901
|
+
async function readHermesConfigDocument2(configPath) {
|
|
20902
|
+
const existingRaw = await readFile14(configPath, "utf8").catch(
|
|
20903
|
+
(error) => {
|
|
20904
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20905
|
+
return null;
|
|
20906
|
+
}
|
|
20907
|
+
throw error;
|
|
20908
|
+
}
|
|
20909
|
+
);
|
|
20910
|
+
const document = existingRaw ? YAML3.parseDocument(existingRaw) : new YAML3.Document({});
|
|
20911
|
+
return {
|
|
20912
|
+
document,
|
|
20913
|
+
config: toRecord15(document.toJSON()),
|
|
20914
|
+
existingRaw
|
|
20915
|
+
};
|
|
20916
|
+
}
|
|
20917
|
+
async function writeHermesConfigDocument2(input) {
|
|
20918
|
+
const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
|
|
20919
|
+
if (backupPath) {
|
|
20920
|
+
await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
|
|
20921
|
+
metadataSourcePath: input.configPath
|
|
20922
|
+
});
|
|
20923
|
+
}
|
|
20924
|
+
input.document.contents = input.document.createNode(input.config);
|
|
20925
|
+
await atomicWriteFilePreservingMetadata(
|
|
20926
|
+
input.configPath,
|
|
20927
|
+
input.document.toString()
|
|
20928
|
+
);
|
|
20929
|
+
return backupPath;
|
|
20930
|
+
}
|
|
20931
|
+
function appendExternalDir(current, externalDir, hermesHome) {
|
|
20932
|
+
const entries = readExternalDirEntries(current);
|
|
20933
|
+
const seen = new Set(
|
|
20934
|
+
entries.map((entry) => resolveExternalDirEntry(entry, hermesHome))
|
|
20935
|
+
);
|
|
20936
|
+
const normalizedExternalDir = path21.resolve(externalDir);
|
|
20937
|
+
return seen.has(normalizedExternalDir) ? entries : [...entries, normalizedExternalDir];
|
|
20938
|
+
}
|
|
20939
|
+
function externalDirsInclude(current, externalDir, hermesHome) {
|
|
20940
|
+
const normalizedExternalDir = path21.resolve(externalDir);
|
|
20941
|
+
return readExternalDirEntries(current).some(
|
|
20942
|
+
(entry) => resolveExternalDirEntry(entry, hermesHome) === normalizedExternalDir
|
|
20943
|
+
);
|
|
20944
|
+
}
|
|
20945
|
+
function readExternalDirEntries(value) {
|
|
20946
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
20947
|
+
return raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
|
|
20948
|
+
}
|
|
20949
|
+
function resolveExternalDirEntry(entry, hermesHome) {
|
|
20950
|
+
const expanded = expandHome(expandEnvVars(entry));
|
|
20951
|
+
return path21.resolve(path21.isAbsolute(expanded) ? expanded : path21.join(hermesHome, expanded));
|
|
20952
|
+
}
|
|
20953
|
+
function expandHome(value) {
|
|
20954
|
+
if (value === "~") {
|
|
20955
|
+
return os4.homedir();
|
|
20956
|
+
}
|
|
20957
|
+
if (value.startsWith(`~${path21.sep}`) || value.startsWith("~/")) {
|
|
20958
|
+
return path21.join(os4.homedir(), value.slice(2));
|
|
20959
|
+
}
|
|
20960
|
+
return value;
|
|
20961
|
+
}
|
|
20962
|
+
function expandEnvVars(value) {
|
|
20963
|
+
return value.replace(
|
|
20964
|
+
/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/gu,
|
|
20965
|
+
(_match, braced, bare) => process.env[braced ?? bare ?? ""] ?? ""
|
|
20966
|
+
);
|
|
20967
|
+
}
|
|
20968
|
+
async function pathIsDirectory(filePath) {
|
|
20969
|
+
return stat14(filePath).then((value) => value.isDirectory()).catch((error) => {
|
|
20970
|
+
if (isNodeError16(error, "ENOENT")) {
|
|
20971
|
+
return false;
|
|
20972
|
+
}
|
|
20973
|
+
throw error;
|
|
20974
|
+
});
|
|
20975
|
+
}
|
|
20976
|
+
function ensureRecord2(target, key) {
|
|
20977
|
+
const existing = target[key];
|
|
20978
|
+
if (isRecord3(existing)) {
|
|
20979
|
+
return existing;
|
|
20980
|
+
}
|
|
20981
|
+
const next = {};
|
|
20982
|
+
target[key] = next;
|
|
20983
|
+
return next;
|
|
20984
|
+
}
|
|
20985
|
+
function toRecord15(value) {
|
|
20986
|
+
return isRecord3(value) ? value : {};
|
|
20987
|
+
}
|
|
20988
|
+
function isRecord3(value) {
|
|
20989
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20990
|
+
}
|
|
20991
|
+
function isNodeError16(error, code) {
|
|
20992
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
20993
|
+
}
|
|
20994
|
+
|
|
20995
|
+
// src/hermes/profile-creation.ts
|
|
20996
|
+
var PROFILE_CREATE_LOG_FILE = "profile-create.log";
|
|
20997
|
+
var PROFILE_CREATE_LOG_MAX_FILES = 3;
|
|
20998
|
+
var MAX_PROFILE_CREATE_LOG_LINES = 260;
|
|
20999
|
+
var MAX_OUTPUT_LINE_LENGTH = 1200;
|
|
21000
|
+
var PROFILE_NAME_PATTERN5 = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
|
|
21001
|
+
var ALL_COPY_SCOPES = [
|
|
21002
|
+
"models",
|
|
21003
|
+
"skills",
|
|
21004
|
+
"tool_permissions",
|
|
21005
|
+
"approval_policy"
|
|
21006
|
+
];
|
|
21007
|
+
var PROFILE_CREATION_EVENTS = new EventEmitter2();
|
|
21008
|
+
var runningProfileCreation = null;
|
|
21009
|
+
async function startHermesProfileCreation(input, options) {
|
|
21010
|
+
const current = await readHermesProfileCreationStatus(options.paths);
|
|
21011
|
+
if (runningProfileCreation || current.state === "running") {
|
|
21012
|
+
return current;
|
|
21013
|
+
}
|
|
21014
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
21015
|
+
const normalized = await normalizeProfileCreationInput(input, options.paths);
|
|
21016
|
+
const profileName = normalized.name ?? await generateProfileName(normalized.displayName, options.paths);
|
|
21017
|
+
const sourceProfile = normalized.copyFrom;
|
|
21018
|
+
const copyScopes = normalized.copyScopes;
|
|
21019
|
+
const jobId = `profile_create_${now().getTime().toString(36)}`;
|
|
21020
|
+
await clearProfileCreationLogFiles(options.paths);
|
|
21021
|
+
const writer = createRotatingTextLogWriter({
|
|
21022
|
+
paths: options.paths,
|
|
21023
|
+
fileName: PROFILE_CREATE_LOG_FILE,
|
|
21024
|
+
maxFileBytes: 512 * 1024,
|
|
19890
21025
|
maxFiles: PROFILE_CREATE_LOG_MAX_FILES
|
|
19891
21026
|
});
|
|
19892
21027
|
const startedAt = now().toISOString();
|
|
@@ -20194,6 +21329,11 @@ async function applyProfileCreationPostSteps(input) {
|
|
|
20194
21329
|
await input.writer.write("Ensuring Hermes API Server config...\n");
|
|
20195
21330
|
await ensureHermesApiServerKey(input.profileName);
|
|
20196
21331
|
await getHermesProfileStatus(input.profileName, input.paths);
|
|
21332
|
+
await input.writer.write("Ensuring Hermes Link skill...\n");
|
|
21333
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
21334
|
+
paths: input.paths,
|
|
21335
|
+
source: "profile_creation"
|
|
21336
|
+
});
|
|
20197
21337
|
if (input.displayName || input.description || input.avatarType === "url" || input.avatarUrl) {
|
|
20198
21338
|
await input.writer.write("Saving Profile display metadata...\n");
|
|
20199
21339
|
await updateProfileMetadata(input.paths, {
|
|
@@ -20246,23 +21386,23 @@ function copyModelConfig(source, target) {
|
|
|
20246
21386
|
copied[key] = cloneJson(source[key]);
|
|
20247
21387
|
}
|
|
20248
21388
|
}
|
|
20249
|
-
const sourceAuxiliary =
|
|
21389
|
+
const sourceAuxiliary = toRecord16(source.auxiliary);
|
|
20250
21390
|
if (Object.prototype.hasOwnProperty.call(sourceAuxiliary, "compression")) {
|
|
20251
|
-
const targetAuxiliary =
|
|
21391
|
+
const targetAuxiliary = ensureRecord3(target, "auxiliary");
|
|
20252
21392
|
targetAuxiliary.compression = cloneJson(sourceAuxiliary.compression);
|
|
20253
21393
|
copied.auxiliary = { compression: cloneJson(sourceAuxiliary.compression) };
|
|
20254
21394
|
}
|
|
20255
21395
|
return copied;
|
|
20256
21396
|
}
|
|
20257
21397
|
function copyToolPermissionsConfig(source, target) {
|
|
20258
|
-
const sourcePlatformToolsets =
|
|
21398
|
+
const sourcePlatformToolsets = toRecord16(source.platform_toolsets);
|
|
20259
21399
|
if (Object.prototype.hasOwnProperty.call(sourcePlatformToolsets, "api_server")) {
|
|
20260
|
-
const targetPlatformToolsets =
|
|
21400
|
+
const targetPlatformToolsets = ensureRecord3(target, "platform_toolsets");
|
|
20261
21401
|
targetPlatformToolsets.api_server = cloneJson(sourcePlatformToolsets.api_server);
|
|
20262
21402
|
}
|
|
20263
|
-
const sourceStt =
|
|
21403
|
+
const sourceStt = toRecord16(source.stt);
|
|
20264
21404
|
if (Object.prototype.hasOwnProperty.call(sourceStt, "enabled")) {
|
|
20265
|
-
const targetStt =
|
|
21405
|
+
const targetStt = ensureRecord3(target, "stt");
|
|
20266
21406
|
targetStt.enabled = cloneJson(sourceStt.enabled);
|
|
20267
21407
|
}
|
|
20268
21408
|
copyProperty(source, target, "command_allowlist");
|
|
@@ -20307,9 +21447,9 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
|
|
|
20307
21447
|
return keys;
|
|
20308
21448
|
}
|
|
20309
21449
|
async function writeEnvValues(profileName, values) {
|
|
20310
|
-
const envPath =
|
|
20311
|
-
const existingRaw = await
|
|
20312
|
-
if (
|
|
21450
|
+
const envPath = path22.join(resolveHermesProfileDir(profileName), ".env");
|
|
21451
|
+
const existingRaw = await readFile15(envPath, "utf8").catch((error) => {
|
|
21452
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20313
21453
|
return "";
|
|
20314
21454
|
}
|
|
20315
21455
|
throw error;
|
|
@@ -20344,8 +21484,8 @@ async function writeEnvValues(profileName, values) {
|
|
|
20344
21484
|
await atomicWriteFilePreservingMetadata(envPath, nextRaw);
|
|
20345
21485
|
}
|
|
20346
21486
|
async function copySkills(sourceProfile, targetProfile) {
|
|
20347
|
-
const sourceSkills =
|
|
20348
|
-
const targetSkills =
|
|
21487
|
+
const sourceSkills = path22.join(resolveHermesProfileDir(sourceProfile), "skills");
|
|
21488
|
+
const targetSkills = path22.join(resolveHermesProfileDir(targetProfile), "skills");
|
|
20349
21489
|
if (!await pathExists2(sourceSkills)) {
|
|
20350
21490
|
return;
|
|
20351
21491
|
}
|
|
@@ -20368,16 +21508,16 @@ function copyProperty(source, target, key) {
|
|
|
20368
21508
|
}
|
|
20369
21509
|
}
|
|
20370
21510
|
async function readYamlConfig(configPath) {
|
|
20371
|
-
const existingRaw = await
|
|
21511
|
+
const existingRaw = await readFile15(configPath, "utf8").catch(
|
|
20372
21512
|
(error) => {
|
|
20373
|
-
if (
|
|
21513
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20374
21514
|
return null;
|
|
20375
21515
|
}
|
|
20376
21516
|
throw error;
|
|
20377
21517
|
}
|
|
20378
21518
|
);
|
|
20379
21519
|
return {
|
|
20380
|
-
config:
|
|
21520
|
+
config: toRecord16(existingRaw ? YAML4.parse(existingRaw) : {}),
|
|
20381
21521
|
existingRaw
|
|
20382
21522
|
};
|
|
20383
21523
|
}
|
|
@@ -20389,7 +21529,7 @@ async function writeYamlConfig(configPath, input) {
|
|
|
20389
21529
|
{ metadataSourcePath: configPath }
|
|
20390
21530
|
);
|
|
20391
21531
|
}
|
|
20392
|
-
const document = new
|
|
21532
|
+
const document = new YAML4.Document(input.config);
|
|
20393
21533
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
20394
21534
|
}
|
|
20395
21535
|
async function failProfileCreation(input) {
|
|
@@ -20431,7 +21571,7 @@ async function writeProfileCreationState(paths, state) {
|
|
|
20431
21571
|
await writeJsonFile(profileCreationStatePath(paths), state);
|
|
20432
21572
|
}
|
|
20433
21573
|
async function readProfileCreationLogLines(paths) {
|
|
20434
|
-
const raw = await
|
|
21574
|
+
const raw = await readFile15(profileCreationLogPath(paths), "utf8").catch(() => "");
|
|
20435
21575
|
if (!raw.trim()) {
|
|
20436
21576
|
return [];
|
|
20437
21577
|
}
|
|
@@ -20440,10 +21580,10 @@ async function readProfileCreationLogLines(paths) {
|
|
|
20440
21580
|
);
|
|
20441
21581
|
}
|
|
20442
21582
|
function profileCreationStatePath(paths) {
|
|
20443
|
-
return
|
|
21583
|
+
return path22.join(paths.runDir, "profile-create-state.json");
|
|
20444
21584
|
}
|
|
20445
21585
|
function profileCreationLogPath(paths) {
|
|
20446
|
-
return
|
|
21586
|
+
return path22.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
|
|
20447
21587
|
}
|
|
20448
21588
|
async function clearProfileCreationLogFiles(paths) {
|
|
20449
21589
|
const primary = profileCreationLogPath(paths);
|
|
@@ -20456,8 +21596,8 @@ async function clearProfileCreationLogFiles(paths) {
|
|
|
20456
21596
|
]);
|
|
20457
21597
|
}
|
|
20458
21598
|
async function pathExists2(targetPath) {
|
|
20459
|
-
return await
|
|
20460
|
-
if (
|
|
21599
|
+
return await stat15(targetPath).then(() => true).catch((error) => {
|
|
21600
|
+
if (isNodeError17(error, "ENOENT")) {
|
|
20461
21601
|
return false;
|
|
20462
21602
|
}
|
|
20463
21603
|
throw error;
|
|
@@ -20480,7 +21620,7 @@ function isProcessAlive(pid) {
|
|
|
20480
21620
|
return false;
|
|
20481
21621
|
}
|
|
20482
21622
|
}
|
|
20483
|
-
function
|
|
21623
|
+
function ensureRecord3(target, key) {
|
|
20484
21624
|
const value = target[key];
|
|
20485
21625
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
20486
21626
|
return value;
|
|
@@ -20489,7 +21629,7 @@ function ensureRecord2(target, key) {
|
|
|
20489
21629
|
target[key] = next;
|
|
20490
21630
|
return next;
|
|
20491
21631
|
}
|
|
20492
|
-
function
|
|
21632
|
+
function toRecord16(value) {
|
|
20493
21633
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
20494
21634
|
}
|
|
20495
21635
|
function cloneJson(value) {
|
|
@@ -20504,7 +21644,7 @@ function formatEnvValue2(value) {
|
|
|
20504
21644
|
function escapeRegExp3(value) {
|
|
20505
21645
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
20506
21646
|
}
|
|
20507
|
-
function
|
|
21647
|
+
function isNodeError17(error, code) {
|
|
20508
21648
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
20509
21649
|
}
|
|
20510
21650
|
|
|
@@ -20714,11 +21854,11 @@ function toProfileToolConfigHttpError(error) {
|
|
|
20714
21854
|
import {
|
|
20715
21855
|
access as access3,
|
|
20716
21856
|
readdir as readdir10,
|
|
20717
|
-
readFile as
|
|
20718
|
-
stat as
|
|
21857
|
+
readFile as readFile16,
|
|
21858
|
+
stat as stat16
|
|
20719
21859
|
} from "fs/promises";
|
|
20720
|
-
import
|
|
20721
|
-
import
|
|
21860
|
+
import path23 from "path";
|
|
21861
|
+
import YAML5 from "yaml";
|
|
20722
21862
|
var ENTRY_DELIMITER = "\n\xA7\n";
|
|
20723
21863
|
var DEFAULT_MEMORY_LIMIT = 2200;
|
|
20724
21864
|
var DEFAULT_USER_LIMIT = 1375;
|
|
@@ -21039,7 +22179,7 @@ async function saveProviderSettings(profileName, provider, patch) {
|
|
|
21039
22179
|
});
|
|
21040
22180
|
await patchJsonProviderConfig(
|
|
21041
22181
|
profileName,
|
|
21042
|
-
|
|
22182
|
+
path23.join("hindsight", "config.json"),
|
|
21043
22183
|
{
|
|
21044
22184
|
mode: patch.mode,
|
|
21045
22185
|
api_url: patch.apiUrl,
|
|
@@ -21096,7 +22236,7 @@ async function patchCustomProviderConfig(profileName, provider, patch) {
|
|
|
21096
22236
|
"\u81EA\u5B9A\u4E49 memory provider \u914D\u7F6E\u5FC5\u987B\u662F\u6709\u6548\u7684 JSON object\u3002"
|
|
21097
22237
|
);
|
|
21098
22238
|
}
|
|
21099
|
-
const config =
|
|
22239
|
+
const config = toRecord17(parsed);
|
|
21100
22240
|
if (Object.keys(config).length === 0 && parsed !== null) {
|
|
21101
22241
|
throw new HermesMemoryError(
|
|
21102
22242
|
"memory_provider_config_invalid",
|
|
@@ -21187,17 +22327,17 @@ function normalizeCustomProviderId(provider) {
|
|
|
21187
22327
|
}
|
|
21188
22328
|
async function patchHermesMemoryProvider(profileName, provider) {
|
|
21189
22329
|
const configPath = resolveHermesConfigPath(profileName);
|
|
21190
|
-
const existingRaw = await
|
|
22330
|
+
const existingRaw = await readFile16(configPath, "utf8").catch(
|
|
21191
22331
|
(error) => {
|
|
21192
|
-
if (
|
|
22332
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21193
22333
|
return null;
|
|
21194
22334
|
}
|
|
21195
22335
|
throw error;
|
|
21196
22336
|
}
|
|
21197
22337
|
);
|
|
21198
|
-
const document = existingRaw ?
|
|
21199
|
-
const config =
|
|
21200
|
-
const memory =
|
|
22338
|
+
const document = existingRaw ? YAML5.parseDocument(existingRaw) : new YAML5.Document({});
|
|
22339
|
+
const config = toRecord17(document.toJSON());
|
|
22340
|
+
const memory = toRecord17(config.memory);
|
|
21201
22341
|
memory.provider = provider === "built-in" ? "" : provider;
|
|
21202
22342
|
config.memory = memory;
|
|
21203
22343
|
const backupPath = existingRaw ? `${configPath}.bak.${Date.now()}` : null;
|
|
@@ -21210,13 +22350,13 @@ async function patchHermesMemoryProvider(profileName, provider) {
|
|
|
21210
22350
|
await atomicWriteFilePreservingMetadata(configPath, document.toString());
|
|
21211
22351
|
}
|
|
21212
22352
|
function resolveMemoryDir(profileName) {
|
|
21213
|
-
return
|
|
22353
|
+
return path23.join(resolveHermesProfileDir(profileName), "memories");
|
|
21214
22354
|
}
|
|
21215
22355
|
async function readMemoryStore(profileName, target, limits) {
|
|
21216
22356
|
const filePath = memoryFilePath(profileName, target);
|
|
21217
22357
|
const entries = await readMemoryEntries(filePath);
|
|
21218
|
-
const fileStat = await
|
|
21219
|
-
if (
|
|
22358
|
+
const fileStat = await stat16(filePath).catch((error) => {
|
|
22359
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21220
22360
|
return null;
|
|
21221
22361
|
}
|
|
21222
22362
|
throw error;
|
|
@@ -21244,8 +22384,8 @@ async function readMemoryStore(profileName, target, limits) {
|
|
|
21244
22384
|
};
|
|
21245
22385
|
}
|
|
21246
22386
|
async function readMemoryEntries(filePath) {
|
|
21247
|
-
const raw = await
|
|
21248
|
-
if (
|
|
22387
|
+
const raw = await readFile16(filePath, "utf8").catch((error) => {
|
|
22388
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21249
22389
|
return "";
|
|
21250
22390
|
}
|
|
21251
22391
|
throw error;
|
|
@@ -21271,7 +22411,7 @@ async function writeMemoryEntries(profileName, target, entries) {
|
|
|
21271
22411
|
);
|
|
21272
22412
|
}
|
|
21273
22413
|
function memoryFilePath(profileName, target) {
|
|
21274
|
-
return
|
|
22414
|
+
return path23.join(
|
|
21275
22415
|
resolveMemoryDir(profileName),
|
|
21276
22416
|
target === "user" ? "USER.md" : "MEMORY.md"
|
|
21277
22417
|
);
|
|
@@ -21304,7 +22444,7 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
|
|
|
21304
22444
|
}
|
|
21305
22445
|
return Promise.all(
|
|
21306
22446
|
[...descriptors.values()].map(async (descriptor) => {
|
|
21307
|
-
const
|
|
22447
|
+
const installed2 = await isUserMemoryProviderInstalled(
|
|
21308
22448
|
profileName,
|
|
21309
22449
|
descriptor.id
|
|
21310
22450
|
);
|
|
@@ -21314,8 +22454,8 @@ async function readCustomProviderSummaries(profileName, activeProviderId) {
|
|
|
21314
22454
|
description: descriptor.description,
|
|
21315
22455
|
active: descriptor.id === activeProviderId,
|
|
21316
22456
|
configurable: true,
|
|
21317
|
-
configured:
|
|
21318
|
-
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",
|
|
21319
22459
|
providerConfigPath: customProviderConfigPath(profileName, descriptor.id),
|
|
21320
22460
|
settings: await readCustomProviderSettings(profileName, descriptor.id)
|
|
21321
22461
|
};
|
|
@@ -21331,7 +22471,7 @@ async function readCustomProviderSetupSummary(profileName) {
|
|
|
21331
22471
|
configurable: true,
|
|
21332
22472
|
configured: true,
|
|
21333
22473
|
configurationIssue: null,
|
|
21334
|
-
providerConfigPath:
|
|
22474
|
+
providerConfigPath: path23.join(
|
|
21335
22475
|
resolveHermesProfileDir(profileName),
|
|
21336
22476
|
"<provider>.json"
|
|
21337
22477
|
),
|
|
@@ -21660,8 +22800,8 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21660
22800
|
memoryProviderConfigPath(profileName, provider) ?? ""
|
|
21661
22801
|
);
|
|
21662
22802
|
const env = await readHermesMemoryEnv(profileName);
|
|
21663
|
-
const banks =
|
|
21664
|
-
const hermesBank =
|
|
22803
|
+
const banks = toRecord17(config.banks);
|
|
22804
|
+
const hermesBank = toRecord17(banks.hermes);
|
|
21665
22805
|
const mode = normalizeHindsightMode(config.mode);
|
|
21666
22806
|
return [
|
|
21667
22807
|
selectSetting("mode", "\u8FDE\u63A5\u6A21\u5F0F", mode, [
|
|
@@ -21725,7 +22865,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21725
22865
|
stringSetting(
|
|
21726
22866
|
"dbPath",
|
|
21727
22867
|
"SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
|
|
21728
|
-
config.db_path ??
|
|
22868
|
+
config.db_path ?? path23.join(resolveHermesProfileDir(profileName), "memory_store.db")
|
|
21729
22869
|
),
|
|
21730
22870
|
booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
|
|
21731
22871
|
numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
|
|
@@ -21761,7 +22901,7 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21761
22901
|
stringSetting(
|
|
21762
22902
|
"workingDirectory",
|
|
21763
22903
|
"\u5DE5\u4F5C\u76EE\u5F55",
|
|
21764
|
-
|
|
22904
|
+
path23.join(resolveHermesProfileDir(profileName), "byterover"),
|
|
21765
22905
|
false
|
|
21766
22906
|
)
|
|
21767
22907
|
];
|
|
@@ -21770,16 +22910,16 @@ async function readProviderSettings(profileName, provider) {
|
|
|
21770
22910
|
}
|
|
21771
22911
|
function memoryProviderConfigPath(profileName, provider) {
|
|
21772
22912
|
if (provider === "honcho") {
|
|
21773
|
-
return
|
|
22913
|
+
return path23.join(resolveHermesProfileDir(profileName), "honcho.json");
|
|
21774
22914
|
}
|
|
21775
22915
|
if (provider === "mem0") {
|
|
21776
|
-
return
|
|
22916
|
+
return path23.join(resolveHermesProfileDir(profileName), "mem0.json");
|
|
21777
22917
|
}
|
|
21778
22918
|
if (provider === "supermemory") {
|
|
21779
|
-
return
|
|
22919
|
+
return path23.join(resolveHermesProfileDir(profileName), "supermemory.json");
|
|
21780
22920
|
}
|
|
21781
22921
|
if (provider === "hindsight") {
|
|
21782
|
-
return
|
|
22922
|
+
return path23.join(
|
|
21783
22923
|
resolveHermesProfileDir(profileName),
|
|
21784
22924
|
"hindsight",
|
|
21785
22925
|
"config.json"
|
|
@@ -21788,21 +22928,21 @@ function memoryProviderConfigPath(profileName, provider) {
|
|
|
21788
22928
|
return null;
|
|
21789
22929
|
}
|
|
21790
22930
|
function customProviderConfigPath(profileName, provider) {
|
|
21791
|
-
return
|
|
22931
|
+
return path23.join(
|
|
21792
22932
|
resolveHermesProfileDir(profileName),
|
|
21793
22933
|
`${normalizeCustomProviderId(provider)}.json`
|
|
21794
22934
|
);
|
|
21795
22935
|
}
|
|
21796
22936
|
function customProviderRegistryPath(profileName) {
|
|
21797
|
-
return
|
|
22937
|
+
return path23.join(
|
|
21798
22938
|
resolveHermesProfileDir(profileName),
|
|
21799
22939
|
CUSTOM_PROVIDER_REGISTRY_FILE
|
|
21800
22940
|
);
|
|
21801
22941
|
}
|
|
21802
22942
|
async function readCustomProviderRegistry(profileName) {
|
|
21803
|
-
const raw = await
|
|
22943
|
+
const raw = await readFile16(customProviderRegistryPath(profileName), "utf8").catch(
|
|
21804
22944
|
(error) => {
|
|
21805
|
-
if (
|
|
22945
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21806
22946
|
return "";
|
|
21807
22947
|
}
|
|
21808
22948
|
throw error;
|
|
@@ -21813,13 +22953,13 @@ async function readCustomProviderRegistry(profileName) {
|
|
|
21813
22953
|
}
|
|
21814
22954
|
try {
|
|
21815
22955
|
const parsed = JSON.parse(raw);
|
|
21816
|
-
const providers = Array.isArray(parsed) ? parsed : Array.isArray(
|
|
22956
|
+
const providers = Array.isArray(parsed) ? parsed : Array.isArray(toRecord17(parsed).providers) ? toRecord17(parsed).providers : [];
|
|
21817
22957
|
return providers.map((item) => {
|
|
21818
22958
|
if (typeof item === "string") {
|
|
21819
22959
|
const id2 = normalizeCustomProviderId(item);
|
|
21820
22960
|
return { id: id2, label: id2, description: "\u81EA\u5B9A\u4E49 memory provider\u3002" };
|
|
21821
22961
|
}
|
|
21822
|
-
const record =
|
|
22962
|
+
const record = toRecord17(item);
|
|
21823
22963
|
const id = normalizeCustomProviderId(readString17(record.id) ?? "");
|
|
21824
22964
|
return {
|
|
21825
22965
|
id,
|
|
@@ -21846,10 +22986,10 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
|
|
|
21846
22986
|
);
|
|
21847
22987
|
}
|
|
21848
22988
|
async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
21849
|
-
const pluginsDir =
|
|
22989
|
+
const pluginsDir = path23.join(resolveHermesProfileDir(profileName), "plugins");
|
|
21850
22990
|
const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
|
|
21851
22991
|
(error) => {
|
|
21852
|
-
if (
|
|
22992
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21853
22993
|
return [];
|
|
21854
22994
|
}
|
|
21855
22995
|
throw error;
|
|
@@ -21866,7 +23006,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
21866
23006
|
} catch {
|
|
21867
23007
|
continue;
|
|
21868
23008
|
}
|
|
21869
|
-
const providerDir =
|
|
23009
|
+
const providerDir = path23.join(pluginsDir, entry.name);
|
|
21870
23010
|
if (!await isMemoryProviderPluginDir(providerDir)) {
|
|
21871
23011
|
continue;
|
|
21872
23012
|
}
|
|
@@ -21880,7 +23020,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
|
|
|
21880
23020
|
return descriptors;
|
|
21881
23021
|
}
|
|
21882
23022
|
async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
21883
|
-
const providerDir =
|
|
23023
|
+
const providerDir = path23.join(
|
|
21884
23024
|
resolveHermesProfileDir(profileName),
|
|
21885
23025
|
"plugins",
|
|
21886
23026
|
normalizeCustomProviderId(provider)
|
|
@@ -21888,9 +23028,9 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
|
|
|
21888
23028
|
return isMemoryProviderPluginDir(providerDir);
|
|
21889
23029
|
}
|
|
21890
23030
|
async function isMemoryProviderPluginDir(providerDir) {
|
|
21891
|
-
const source = await
|
|
23031
|
+
const source = await readFile16(path23.join(providerDir, "__init__.py"), "utf8").catch(
|
|
21892
23032
|
(error) => {
|
|
21893
|
-
if (
|
|
23033
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21894
23034
|
return "";
|
|
21895
23035
|
}
|
|
21896
23036
|
throw error;
|
|
@@ -21900,22 +23040,22 @@ async function isMemoryProviderPluginDir(providerDir) {
|
|
|
21900
23040
|
return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
|
|
21901
23041
|
}
|
|
21902
23042
|
async function readPluginMetadata(providerDir) {
|
|
21903
|
-
const raw = await
|
|
23043
|
+
const raw = await readFile16(path23.join(providerDir, "plugin.yaml"), "utf8").catch(
|
|
21904
23044
|
(error) => {
|
|
21905
|
-
if (
|
|
23045
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21906
23046
|
return "";
|
|
21907
23047
|
}
|
|
21908
23048
|
throw error;
|
|
21909
23049
|
}
|
|
21910
23050
|
);
|
|
21911
|
-
return raw ?
|
|
23051
|
+
return raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
21912
23052
|
}
|
|
21913
23053
|
async function resolveByteRoverCli() {
|
|
21914
23054
|
const candidates = [
|
|
21915
|
-
...(process.env.PATH ?? "").split(
|
|
21916
|
-
|
|
23055
|
+
...(process.env.PATH ?? "").split(path23.delimiter).filter(Boolean).map((dir) => path23.join(dir, "brv")),
|
|
23056
|
+
path23.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
|
|
21917
23057
|
"/usr/local/bin/brv",
|
|
21918
|
-
|
|
23058
|
+
path23.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
|
|
21919
23059
|
].filter(Boolean);
|
|
21920
23060
|
for (const candidate of candidates) {
|
|
21921
23061
|
const found = await access3(candidate).then(() => true).catch(() => false);
|
|
@@ -21926,32 +23066,32 @@ async function resolveByteRoverCli() {
|
|
|
21926
23066
|
return null;
|
|
21927
23067
|
}
|
|
21928
23068
|
async function readHolographicProviderConfig(profileName) {
|
|
21929
|
-
const raw = await
|
|
23069
|
+
const raw = await readFile16(resolveHermesConfigPath(profileName), "utf8").catch(
|
|
21930
23070
|
(error) => {
|
|
21931
|
-
if (
|
|
23071
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21932
23072
|
return "";
|
|
21933
23073
|
}
|
|
21934
23074
|
throw error;
|
|
21935
23075
|
}
|
|
21936
23076
|
);
|
|
21937
|
-
const config = raw ?
|
|
21938
|
-
const plugins =
|
|
21939
|
-
return
|
|
23077
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23078
|
+
const plugins = toRecord17(config.plugins);
|
|
23079
|
+
return toRecord17(plugins["hermes-memory-store"]);
|
|
21940
23080
|
}
|
|
21941
23081
|
async function patchHolographicProviderConfig(profileName, patch) {
|
|
21942
23082
|
const configPath = resolveHermesConfigPath(profileName);
|
|
21943
|
-
const existingRaw = await
|
|
23083
|
+
const existingRaw = await readFile16(configPath, "utf8").catch(
|
|
21944
23084
|
(error) => {
|
|
21945
|
-
if (
|
|
23085
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21946
23086
|
return null;
|
|
21947
23087
|
}
|
|
21948
23088
|
throw error;
|
|
21949
23089
|
}
|
|
21950
23090
|
);
|
|
21951
|
-
const document = existingRaw ?
|
|
21952
|
-
const config =
|
|
21953
|
-
const plugins =
|
|
21954
|
-
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"]);
|
|
21955
23095
|
for (const [key, value] of Object.entries(patch)) {
|
|
21956
23096
|
if (value !== void 0) {
|
|
21957
23097
|
memoryStore[key] = value;
|
|
@@ -21976,9 +23116,9 @@ async function patchHermesMemoryEnv(profileName, patch) {
|
|
|
21976
23116
|
if (entries.length === 0) {
|
|
21977
23117
|
return;
|
|
21978
23118
|
}
|
|
21979
|
-
const envPath =
|
|
21980
|
-
const existingRaw = await
|
|
21981
|
-
if (
|
|
23119
|
+
const envPath = path23.join(resolveHermesProfileDir(profileName), ".env");
|
|
23120
|
+
const existingRaw = await readFile16(envPath, "utf8").catch((error) => {
|
|
23121
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
21982
23122
|
return "";
|
|
21983
23123
|
}
|
|
21984
23124
|
throw error;
|
|
@@ -22099,7 +23239,7 @@ function joinHindsightUrl(baseUrl, pathName) {
|
|
|
22099
23239
|
}
|
|
22100
23240
|
function parseJsonObject2(text) {
|
|
22101
23241
|
try {
|
|
22102
|
-
return
|
|
23242
|
+
return toRecord17(JSON.parse(text));
|
|
22103
23243
|
} catch {
|
|
22104
23244
|
return {};
|
|
22105
23245
|
}
|
|
@@ -22129,17 +23269,17 @@ function summarizeHindsightProbe(pathName, json) {
|
|
|
22129
23269
|
return bankId ? `bank ${bankId} reachable` : "bank config reachable";
|
|
22130
23270
|
}
|
|
22131
23271
|
async function readActiveMemoryProvider(profileName) {
|
|
22132
|
-
const raw = await
|
|
23272
|
+
const raw = await readFile16(
|
|
22133
23273
|
resolveHermesConfigPath(profileName),
|
|
22134
23274
|
"utf8"
|
|
22135
23275
|
).catch((error) => {
|
|
22136
|
-
if (
|
|
23276
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22137
23277
|
return "";
|
|
22138
23278
|
}
|
|
22139
23279
|
throw error;
|
|
22140
23280
|
});
|
|
22141
|
-
const config = raw ?
|
|
22142
|
-
const memory =
|
|
23281
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23282
|
+
const memory = toRecord17(config.memory);
|
|
22143
23283
|
const provider = readString17(memory.provider);
|
|
22144
23284
|
if (!provider || provider === "built-in" || provider === "builtin" || provider === "built_in") {
|
|
22145
23285
|
return null;
|
|
@@ -22147,7 +23287,7 @@ async function readActiveMemoryProvider(profileName) {
|
|
|
22147
23287
|
return provider;
|
|
22148
23288
|
}
|
|
22149
23289
|
async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
22150
|
-
const configPath =
|
|
23290
|
+
const configPath = path23.join(
|
|
22151
23291
|
resolveHermesProfileDir(profileName),
|
|
22152
23292
|
relativePath
|
|
22153
23293
|
);
|
|
@@ -22165,18 +23305,18 @@ async function patchJsonProviderConfig(profileName, relativePath, patch) {
|
|
|
22165
23305
|
);
|
|
22166
23306
|
}
|
|
22167
23307
|
async function readJsonObject(filePath) {
|
|
22168
|
-
const raw = await
|
|
22169
|
-
if (
|
|
23308
|
+
const raw = await readFile16(filePath, "utf8").catch((error) => {
|
|
23309
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22170
23310
|
return "{}";
|
|
22171
23311
|
}
|
|
22172
23312
|
throw error;
|
|
22173
23313
|
});
|
|
22174
23314
|
try {
|
|
22175
|
-
return
|
|
23315
|
+
return toRecord17(JSON.parse(raw || "{}"));
|
|
22176
23316
|
} catch {
|
|
22177
23317
|
throw new HermesMemoryError(
|
|
22178
23318
|
"memory_provider_config_invalid",
|
|
22179
|
-
`${
|
|
23319
|
+
`${path23.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
|
|
22180
23320
|
);
|
|
22181
23321
|
}
|
|
22182
23322
|
}
|
|
@@ -22226,17 +23366,17 @@ function selectSetting(key, label, value, options, editable = true) {
|
|
|
22226
23366
|
return { key, label, value: stringValue, editable, kind: "select", options };
|
|
22227
23367
|
}
|
|
22228
23368
|
async function readMemoryLimits(profileName) {
|
|
22229
|
-
const raw = await
|
|
23369
|
+
const raw = await readFile16(
|
|
22230
23370
|
resolveHermesConfigPath(profileName),
|
|
22231
23371
|
"utf8"
|
|
22232
23372
|
).catch((error) => {
|
|
22233
|
-
if (
|
|
23373
|
+
if (isNodeError18(error, "ENOENT")) {
|
|
22234
23374
|
return "";
|
|
22235
23375
|
}
|
|
22236
23376
|
throw error;
|
|
22237
23377
|
});
|
|
22238
|
-
const config = raw ?
|
|
22239
|
-
const memory =
|
|
23378
|
+
const config = raw ? toRecord17(YAML5.parse(raw)) : {};
|
|
23379
|
+
const memory = toRecord17(config.memory);
|
|
22240
23380
|
return {
|
|
22241
23381
|
memory: readPositiveInteger3(memory.memory_char_limit) ?? DEFAULT_MEMORY_LIMIT,
|
|
22242
23382
|
user: readPositiveInteger3(memory.user_char_limit) ?? DEFAULT_USER_LIMIT
|
|
@@ -22292,7 +23432,7 @@ function hashString(value) {
|
|
|
22292
23432
|
}
|
|
22293
23433
|
return hash.toString(16);
|
|
22294
23434
|
}
|
|
22295
|
-
function
|
|
23435
|
+
function toRecord17(value) {
|
|
22296
23436
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
22297
23437
|
}
|
|
22298
23438
|
function readString17(value) {
|
|
@@ -22323,7 +23463,7 @@ function formatEnvValue3(value) {
|
|
|
22323
23463
|
function escapeRegExp4(value) {
|
|
22324
23464
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
22325
23465
|
}
|
|
22326
|
-
function
|
|
23466
|
+
function isNodeError18(error, code) {
|
|
22327
23467
|
return error instanceof Error && "code" in error && error.code === code;
|
|
22328
23468
|
}
|
|
22329
23469
|
|
|
@@ -22764,9 +23904,9 @@ function toMemoryHttpError(error) {
|
|
|
22764
23904
|
}
|
|
22765
23905
|
|
|
22766
23906
|
// src/hermes/skills.ts
|
|
22767
|
-
import { readFile as
|
|
22768
|
-
import
|
|
22769
|
-
import
|
|
23907
|
+
import { readFile as readFile17, readdir as readdir11 } from "fs/promises";
|
|
23908
|
+
import path24 from "path";
|
|
23909
|
+
import YAML6 from "yaml";
|
|
22770
23910
|
var HermesSkillNotFoundError = class extends Error {
|
|
22771
23911
|
constructor(skillName) {
|
|
22772
23912
|
super(`skill "${skillName}" does not exist`);
|
|
@@ -22779,7 +23919,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
|
|
|
22779
23919
|
async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
|
|
22780
23920
|
const profile = await readExistingProfile(profileName, paths);
|
|
22781
23921
|
const profileDir = resolveHermesProfileDir(profile.name);
|
|
22782
|
-
const skillsRoot =
|
|
23922
|
+
const skillsRoot = path24.join(profileDir, "skills");
|
|
22783
23923
|
const [skillFiles, disabled, provenance] = await Promise.all([
|
|
22784
23924
|
findSkillFiles(skillsRoot),
|
|
22785
23925
|
readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
|
|
@@ -22815,8 +23955,8 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
|
|
|
22815
23955
|
throw new HermesSkillNotFoundError(skillName);
|
|
22816
23956
|
}
|
|
22817
23957
|
const configPath = resolveHermesConfigPath(current.profile.name);
|
|
22818
|
-
const { document, config, existingRaw } = await
|
|
22819
|
-
const skillsConfig =
|
|
23958
|
+
const { document, config, existingRaw } = await readHermesConfigDocument3(configPath);
|
|
23959
|
+
const skillsConfig = ensureRecord4(config, "skills");
|
|
22820
23960
|
const disabled = new Set(readStringList3(skillsConfig.disabled));
|
|
22821
23961
|
if (enabled) {
|
|
22822
23962
|
disabled.delete(target.name);
|
|
@@ -22826,7 +23966,7 @@ async function setHermesProfileSkillEnabled(profileName, skillName, enabled, pat
|
|
|
22826
23966
|
skillsConfig.disabled = [...disabled].sort(
|
|
22827
23967
|
(left, right) => left.localeCompare(right)
|
|
22828
23968
|
);
|
|
22829
|
-
const backupPath = await
|
|
23969
|
+
const backupPath = await writeHermesConfigDocument3({
|
|
22830
23970
|
configPath,
|
|
22831
23971
|
document,
|
|
22832
23972
|
config,
|
|
@@ -22858,7 +23998,7 @@ async function findSkillFiles(root) {
|
|
|
22858
23998
|
async function collectSkillFiles(directory, results) {
|
|
22859
23999
|
const entries = await readdir11(directory, { withFileTypes: true }).catch(
|
|
22860
24000
|
(error) => {
|
|
22861
|
-
if (
|
|
24001
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
22862
24002
|
return [];
|
|
22863
24003
|
}
|
|
22864
24004
|
throw error;
|
|
@@ -22870,7 +24010,7 @@ async function collectSkillFiles(directory, results) {
|
|
|
22870
24010
|
if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
|
|
22871
24011
|
continue;
|
|
22872
24012
|
}
|
|
22873
|
-
const entryPath =
|
|
24013
|
+
const entryPath = path24.join(directory, entry.name);
|
|
22874
24014
|
if (entry.isDirectory()) {
|
|
22875
24015
|
await collectSkillFiles(entryPath, results);
|
|
22876
24016
|
continue;
|
|
@@ -22881,9 +24021,9 @@ async function collectSkillFiles(directory, results) {
|
|
|
22881
24021
|
}
|
|
22882
24022
|
}
|
|
22883
24023
|
async function readSkillMetadata(input) {
|
|
22884
|
-
const raw = await
|
|
24024
|
+
const raw = await readFile17(input.skillFile, "utf8").catch(
|
|
22885
24025
|
(error) => {
|
|
22886
|
-
if (
|
|
24026
|
+
if (isNodeError19(error, "ENOENT") || isNodeError19(error, "EACCES")) {
|
|
22887
24027
|
return null;
|
|
22888
24028
|
}
|
|
22889
24029
|
throw error;
|
|
@@ -22892,10 +24032,10 @@ async function readSkillMetadata(input) {
|
|
|
22892
24032
|
if (raw === null) {
|
|
22893
24033
|
return null;
|
|
22894
24034
|
}
|
|
22895
|
-
const skillDir =
|
|
24035
|
+
const skillDir = path24.dirname(input.skillFile);
|
|
22896
24036
|
const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
|
|
22897
24037
|
const name = normalizeSkillName(
|
|
22898
|
-
readString18(frontmatter.name) ??
|
|
24038
|
+
readString18(frontmatter.name) ?? path24.basename(skillDir)
|
|
22899
24039
|
);
|
|
22900
24040
|
if (!name) {
|
|
22901
24041
|
return null;
|
|
@@ -22914,7 +24054,7 @@ async function readSkillMetadata(input) {
|
|
|
22914
24054
|
enabled: !input.disabled.has(name),
|
|
22915
24055
|
source: provenance.source,
|
|
22916
24056
|
trust: provenance.trust,
|
|
22917
|
-
relativePath:
|
|
24057
|
+
relativePath: path24.relative(input.skillsRoot, skillDir)
|
|
22918
24058
|
};
|
|
22919
24059
|
}
|
|
22920
24060
|
function parseSkillDocument(raw) {
|
|
@@ -22927,7 +24067,7 @@ function parseSkillDocument(raw) {
|
|
|
22927
24067
|
}
|
|
22928
24068
|
try {
|
|
22929
24069
|
return {
|
|
22930
|
-
frontmatter:
|
|
24070
|
+
frontmatter: toRecord18(YAML6.parse(match[1] ?? "")),
|
|
22931
24071
|
body: content.slice(match[0].length)
|
|
22932
24072
|
};
|
|
22933
24073
|
} catch {
|
|
@@ -22935,8 +24075,8 @@ function parseSkillDocument(raw) {
|
|
|
22935
24075
|
}
|
|
22936
24076
|
}
|
|
22937
24077
|
function categoryFromPath(skillsRoot, skillFile) {
|
|
22938
|
-
const relative =
|
|
22939
|
-
const parts = relative.split(
|
|
24078
|
+
const relative = path24.relative(skillsRoot, skillFile);
|
|
24079
|
+
const parts = relative.split(path24.sep).filter(Boolean);
|
|
22940
24080
|
return parts.length >= 3 ? parts[0] : null;
|
|
22941
24081
|
}
|
|
22942
24082
|
function firstBodyDescription(body) {
|
|
@@ -22959,8 +24099,8 @@ function normalizeDescription(value) {
|
|
|
22959
24099
|
return `${description.slice(0, MAX_DESCRIPTION_LENGTH - 3)}...`;
|
|
22960
24100
|
}
|
|
22961
24101
|
async function readDisabledSkillNames(configPath) {
|
|
22962
|
-
const raw = await
|
|
22963
|
-
if (
|
|
24102
|
+
const raw = await readFile17(configPath, "utf8").catch((error) => {
|
|
24103
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
22964
24104
|
return "";
|
|
22965
24105
|
}
|
|
22966
24106
|
throw error;
|
|
@@ -22968,8 +24108,8 @@ async function readDisabledSkillNames(configPath) {
|
|
|
22968
24108
|
if (!raw.trim()) {
|
|
22969
24109
|
return /* @__PURE__ */ new Set();
|
|
22970
24110
|
}
|
|
22971
|
-
const config =
|
|
22972
|
-
const skills =
|
|
24111
|
+
const config = toRecord18(YAML6.parse(raw));
|
|
24112
|
+
const skills = toRecord18(config.skills);
|
|
22973
24113
|
return new Set(readStringList3(skills.disabled));
|
|
22974
24114
|
}
|
|
22975
24115
|
async function readSkillProvenance(root) {
|
|
@@ -22983,9 +24123,9 @@ async function readSkillProvenance(root) {
|
|
|
22983
24123
|
return provenance;
|
|
22984
24124
|
}
|
|
22985
24125
|
async function readBundledSkillNames(root) {
|
|
22986
|
-
const raw = await
|
|
24126
|
+
const raw = await readFile17(path24.join(root, ".bundled_manifest"), "utf8").catch(
|
|
22987
24127
|
(error) => {
|
|
22988
|
-
if (
|
|
24128
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
22989
24129
|
return "";
|
|
22990
24130
|
}
|
|
22991
24131
|
throw error;
|
|
@@ -23006,9 +24146,9 @@ async function readBundledSkillNames(root) {
|
|
|
23006
24146
|
return names;
|
|
23007
24147
|
}
|
|
23008
24148
|
async function readHubInstalledSkills(root) {
|
|
23009
|
-
const raw = await
|
|
24149
|
+
const raw = await readFile17(path24.join(root, ".hub", "lock.json"), "utf8").catch(
|
|
23010
24150
|
(error) => {
|
|
23011
|
-
if (
|
|
24151
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23012
24152
|
return "";
|
|
23013
24153
|
}
|
|
23014
24154
|
throw error;
|
|
@@ -23019,14 +24159,14 @@ async function readHubInstalledSkills(root) {
|
|
|
23019
24159
|
}
|
|
23020
24160
|
let lock;
|
|
23021
24161
|
try {
|
|
23022
|
-
lock =
|
|
24162
|
+
lock = toRecord18(JSON.parse(raw));
|
|
23023
24163
|
} catch {
|
|
23024
24164
|
return /* @__PURE__ */ new Map();
|
|
23025
24165
|
}
|
|
23026
|
-
const
|
|
24166
|
+
const installed2 = toRecord18(lock.installed);
|
|
23027
24167
|
const result = /* @__PURE__ */ new Map();
|
|
23028
|
-
for (const [name, rawEntry] of Object.entries(
|
|
23029
|
-
const entry =
|
|
24168
|
+
for (const [name, rawEntry] of Object.entries(installed2)) {
|
|
24169
|
+
const entry = toRecord18(rawEntry);
|
|
23030
24170
|
result.set(normalizeSkillName(name), {
|
|
23031
24171
|
source: readString18(entry.source) ?? "hub",
|
|
23032
24172
|
trust: readString18(entry.trust_level) ?? null
|
|
@@ -23077,23 +24217,23 @@ function compareCategoryNames(left, right) {
|
|
|
23077
24217
|
}
|
|
23078
24218
|
return left.localeCompare(right);
|
|
23079
24219
|
}
|
|
23080
|
-
async function
|
|
23081
|
-
const existingRaw = await
|
|
24220
|
+
async function readHermesConfigDocument3(configPath) {
|
|
24221
|
+
const existingRaw = await readFile17(configPath, "utf8").catch(
|
|
23082
24222
|
(error) => {
|
|
23083
|
-
if (
|
|
24223
|
+
if (isNodeError19(error, "ENOENT")) {
|
|
23084
24224
|
return null;
|
|
23085
24225
|
}
|
|
23086
24226
|
throw error;
|
|
23087
24227
|
}
|
|
23088
24228
|
);
|
|
23089
|
-
const document = existingRaw ?
|
|
24229
|
+
const document = existingRaw ? YAML6.parseDocument(existingRaw) : new YAML6.Document({});
|
|
23090
24230
|
return {
|
|
23091
24231
|
document,
|
|
23092
|
-
config:
|
|
24232
|
+
config: toRecord18(document.toJSON()),
|
|
23093
24233
|
existingRaw
|
|
23094
24234
|
};
|
|
23095
24235
|
}
|
|
23096
|
-
async function
|
|
24236
|
+
async function writeHermesConfigDocument3(input) {
|
|
23097
24237
|
const backupPath = input.existingRaw ? `${input.configPath}.bak.${Date.now()}` : null;
|
|
23098
24238
|
if (backupPath) {
|
|
23099
24239
|
await atomicWriteFilePreservingMetadata(backupPath, input.existingRaw, {
|
|
@@ -23116,18 +24256,18 @@ function readStringList3(value) {
|
|
|
23116
24256
|
function readString18(value) {
|
|
23117
24257
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
23118
24258
|
}
|
|
23119
|
-
function
|
|
24259
|
+
function toRecord18(value) {
|
|
23120
24260
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
23121
24261
|
}
|
|
23122
|
-
function
|
|
23123
|
-
const current =
|
|
24262
|
+
function ensureRecord4(target, key) {
|
|
24263
|
+
const current = toRecord18(target[key]);
|
|
23124
24264
|
if (current === target[key]) {
|
|
23125
24265
|
return current;
|
|
23126
24266
|
}
|
|
23127
24267
|
target[key] = current;
|
|
23128
24268
|
return current;
|
|
23129
24269
|
}
|
|
23130
|
-
function
|
|
24270
|
+
function isNodeError19(error, code) {
|
|
23131
24271
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
23132
24272
|
}
|
|
23133
24273
|
|
|
@@ -23610,8 +24750,8 @@ function readModelList(payload) {
|
|
|
23610
24750
|
// src/hermes/updates.ts
|
|
23611
24751
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
23612
24752
|
import { spawn as spawn3 } from "child_process";
|
|
23613
|
-
import { mkdir as mkdir12, readFile as
|
|
23614
|
-
import
|
|
24753
|
+
import { mkdir as mkdir12, readFile as readFile18, rm as rm7 } from "fs/promises";
|
|
24754
|
+
import path25 from "path";
|
|
23615
24755
|
var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
|
|
23616
24756
|
var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
23617
24757
|
var RELEASE_FETCH_TIMEOUT_MS = 5e3;
|
|
@@ -23844,7 +24984,7 @@ async function readRemoteRelease(options, now) {
|
|
|
23844
24984
|
}
|
|
23845
24985
|
}
|
|
23846
24986
|
function normalizeServerReleaseSnapshot(payload) {
|
|
23847
|
-
const snapshot =
|
|
24987
|
+
const snapshot = toRecord19(payload);
|
|
23848
24988
|
const remote = toNullableRecord(snapshot.remote);
|
|
23849
24989
|
return {
|
|
23850
24990
|
remote: remote ? normalizeServerRelease(remote) : null,
|
|
@@ -23880,7 +25020,7 @@ async function writeUpdateState(paths, state) {
|
|
|
23880
25020
|
await writeJsonFile(updateStatePath(paths), state);
|
|
23881
25021
|
}
|
|
23882
25022
|
async function readUpdateLogLines(paths) {
|
|
23883
|
-
const raw = await
|
|
25023
|
+
const raw = await readFile18(updateLogPath(paths), "utf8").catch(() => "");
|
|
23884
25024
|
if (!raw.trim()) {
|
|
23885
25025
|
return [];
|
|
23886
25026
|
}
|
|
@@ -23889,13 +25029,13 @@ async function readUpdateLogLines(paths) {
|
|
|
23889
25029
|
);
|
|
23890
25030
|
}
|
|
23891
25031
|
function releaseCachePath(paths) {
|
|
23892
|
-
return
|
|
25032
|
+
return path25.join(paths.indexesDir, "hermes-release-check.json");
|
|
23893
25033
|
}
|
|
23894
25034
|
function updateStatePath(paths) {
|
|
23895
|
-
return
|
|
25035
|
+
return path25.join(paths.runDir, "hermes-update-state.json");
|
|
23896
25036
|
}
|
|
23897
25037
|
function updateLogPath(paths) {
|
|
23898
|
-
return
|
|
25038
|
+
return path25.join(paths.logsDir, UPDATE_LOG_FILE);
|
|
23899
25039
|
}
|
|
23900
25040
|
async function clearUpdateLogFiles(paths) {
|
|
23901
25041
|
const primary = updateLogPath(paths);
|
|
@@ -23933,7 +25073,7 @@ function compareSemver2(left, right) {
|
|
|
23933
25073
|
}
|
|
23934
25074
|
return 0;
|
|
23935
25075
|
}
|
|
23936
|
-
function
|
|
25076
|
+
function toRecord19(value) {
|
|
23937
25077
|
return typeof value === "object" && value !== null ? value : {};
|
|
23938
25078
|
}
|
|
23939
25079
|
function toNullableRecord(value) {
|
|
@@ -23995,13 +25135,13 @@ function readString19(payload, key) {
|
|
|
23995
25135
|
// src/link/updates.ts
|
|
23996
25136
|
import { spawn as spawn5 } from "child_process";
|
|
23997
25137
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
23998
|
-
import { mkdir as mkdir15, readFile as
|
|
23999
|
-
import
|
|
25138
|
+
import { mkdir as mkdir15, readFile as readFile20, rm as rm10 } from "fs/promises";
|
|
25139
|
+
import path27 from "path";
|
|
24000
25140
|
|
|
24001
25141
|
// src/daemon/process.ts
|
|
24002
25142
|
import { spawn as spawn4 } from "child_process";
|
|
24003
|
-
import { mkdir as mkdir14, readFile as
|
|
24004
|
-
import
|
|
25143
|
+
import { mkdir as mkdir14, readFile as readFile19, rm as rm9, writeFile as writeFile4 } from "fs/promises";
|
|
25144
|
+
import path26 from "path";
|
|
24005
25145
|
|
|
24006
25146
|
// src/daemon/service.ts
|
|
24007
25147
|
import { createServer } from "http";
|
|
@@ -24010,31 +25150,146 @@ import { mkdir as mkdir13, rm as rm8, writeFile as writeFile3 } from "fs/promise
|
|
|
24010
25150
|
// src/relay/control-client.ts
|
|
24011
25151
|
import WebSocket from "ws";
|
|
24012
25152
|
|
|
24013
|
-
// src/relay/
|
|
24014
|
-
var
|
|
24015
|
-
|
|
24016
|
-
|
|
24017
|
-
|
|
24018
|
-
var
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
max: 64 * 1024
|
|
25153
|
+
// src/relay/reconnect-state.ts
|
|
25154
|
+
var DEFAULT_STORM_WINDOW_MS = 5 * 6e4;
|
|
25155
|
+
var DEFAULT_STORM_DISCONNECT_LIMIT = 8;
|
|
25156
|
+
var DEFAULT_COOLDOWN_MS = 3 * 6e4;
|
|
25157
|
+
var DEFAULT_RELAY_RECONNECT_BASE_MS = 3e3;
|
|
25158
|
+
var DEFAULT_RELAY_RECONNECT_MAX_MS = 6e4;
|
|
25159
|
+
async function readRelayCooldownDelayMs(paths, now = /* @__PURE__ */ new Date()) {
|
|
25160
|
+
const state = await readLinkState(paths);
|
|
25161
|
+
const reconnect = normalizeRelayReconnectState(state.relayReconnect);
|
|
25162
|
+
const cooldownUntilMs = parseTimeMs(reconnect.cooldownUntil);
|
|
25163
|
+
if (!Number.isFinite(cooldownUntilMs)) {
|
|
25164
|
+
return 0;
|
|
24026
25165
|
}
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
const
|
|
24031
|
-
const
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
25166
|
+
return Math.max(0, cooldownUntilMs - now.getTime());
|
|
25167
|
+
}
|
|
25168
|
+
async function recordRelayDisconnect(paths, options = {}) {
|
|
25169
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
25170
|
+
const nowMs = now.getTime();
|
|
25171
|
+
const stormWindowMs = positiveInteger(options.stormWindowMs, DEFAULT_STORM_WINDOW_MS);
|
|
25172
|
+
const stormDisconnectLimit = positiveInteger(
|
|
25173
|
+
options.stormDisconnectLimit,
|
|
25174
|
+
DEFAULT_STORM_DISCONNECT_LIMIT
|
|
25175
|
+
);
|
|
25176
|
+
const cooldownMs = positiveInteger(options.cooldownMs, DEFAULT_COOLDOWN_MS);
|
|
25177
|
+
let result = {
|
|
25178
|
+
disconnectCount: 0,
|
|
25179
|
+
cooldownUntilMs: null
|
|
25180
|
+
};
|
|
25181
|
+
await updateRelayReconnectState(paths, (current) => {
|
|
25182
|
+
const recentDisconnects = [
|
|
25183
|
+
...current.recentDisconnects.filter((value) => {
|
|
25184
|
+
const timestamp = parseTimeMs(value);
|
|
25185
|
+
return Number.isFinite(timestamp) && nowMs - timestamp <= stormWindowMs;
|
|
25186
|
+
}),
|
|
25187
|
+
now.toISOString()
|
|
25188
|
+
];
|
|
25189
|
+
const enteredCooldown = recentDisconnects.length >= stormDisconnectLimit;
|
|
25190
|
+
const cooldownUntil = enteredCooldown ? new Date(nowMs + cooldownMs).toISOString() : current.cooldownUntil;
|
|
25191
|
+
const cooldownUntilMs = enteredCooldown ? nowMs + cooldownMs : null;
|
|
25192
|
+
result = {
|
|
25193
|
+
disconnectCount: recentDisconnects.length,
|
|
25194
|
+
cooldownUntilMs
|
|
25195
|
+
};
|
|
25196
|
+
return {
|
|
25197
|
+
recentDisconnects: enteredCooldown ? [] : recentDisconnects,
|
|
25198
|
+
cooldownUntil,
|
|
25199
|
+
lastFailureAt: now.toISOString(),
|
|
25200
|
+
lastFailureReason: normalizeReason(options.reason)
|
|
25201
|
+
};
|
|
25202
|
+
});
|
|
25203
|
+
return result;
|
|
25204
|
+
}
|
|
25205
|
+
async function clearRelayReconnectState(paths) {
|
|
25206
|
+
await updateRelayReconnectState(paths, (current) => ({
|
|
25207
|
+
...current,
|
|
25208
|
+
recentDisconnects: [],
|
|
25209
|
+
cooldownUntil: null
|
|
25210
|
+
}));
|
|
25211
|
+
}
|
|
25212
|
+
function computeRelayBackoffMs(attempt, options = {}) {
|
|
25213
|
+
const baseMs = positiveInteger(options.baseMs, DEFAULT_RELAY_RECONNECT_BASE_MS);
|
|
25214
|
+
const maxMs = positiveInteger(options.maxMs, DEFAULT_RELAY_RECONNECT_MAX_MS);
|
|
25215
|
+
const normalizedAttempt = Math.max(1, Math.floor(attempt));
|
|
25216
|
+
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, normalizedAttempt - 1));
|
|
25217
|
+
const random = options.random ?? Math.random;
|
|
25218
|
+
const ratio = 0.2 + clampRandom(random()) * 0.1;
|
|
25219
|
+
return exponential + Math.floor(exponential * ratio);
|
|
25220
|
+
}
|
|
25221
|
+
async function updateRelayReconnectState(paths, update) {
|
|
25222
|
+
const state = await readLinkState(paths);
|
|
25223
|
+
const next = {
|
|
25224
|
+
...state,
|
|
25225
|
+
relayReconnect: update(normalizeRelayReconnectState(state.relayReconnect))
|
|
25226
|
+
};
|
|
25227
|
+
await writeJsonFile(paths.stateFile, next);
|
|
25228
|
+
}
|
|
25229
|
+
async function readLinkState(paths) {
|
|
25230
|
+
const state = await readJsonFile(paths.stateFile);
|
|
25231
|
+
return state && typeof state === "object" ? state : {};
|
|
25232
|
+
}
|
|
25233
|
+
function normalizeRelayReconnectState(value) {
|
|
25234
|
+
const record = value && typeof value === "object" ? value : {};
|
|
25235
|
+
return {
|
|
25236
|
+
recentDisconnects: normalizeTimestamps(record.recentDisconnects),
|
|
25237
|
+
cooldownUntil: typeof record.cooldownUntil === "string" ? record.cooldownUntil : null,
|
|
25238
|
+
lastFailureAt: typeof record.lastFailureAt === "string" ? record.lastFailureAt : null,
|
|
25239
|
+
lastFailureReason: typeof record.lastFailureReason === "string" ? record.lastFailureReason : null
|
|
25240
|
+
};
|
|
25241
|
+
}
|
|
25242
|
+
function normalizeTimestamps(value) {
|
|
25243
|
+
if (!Array.isArray(value)) {
|
|
25244
|
+
return [];
|
|
25245
|
+
}
|
|
25246
|
+
return value.filter((item) => typeof item === "string" && Number.isFinite(parseTimeMs(item)));
|
|
25247
|
+
}
|
|
25248
|
+
function normalizeReason(value) {
|
|
25249
|
+
if (typeof value !== "string") {
|
|
25250
|
+
return null;
|
|
25251
|
+
}
|
|
25252
|
+
const trimmed = value.trim();
|
|
25253
|
+
return trimmed ? trimmed.slice(0, 240) : null;
|
|
25254
|
+
}
|
|
25255
|
+
function positiveInteger(value, fallback) {
|
|
25256
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
|
|
25257
|
+
}
|
|
25258
|
+
function parseTimeMs(value) {
|
|
25259
|
+
return typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
25260
|
+
}
|
|
25261
|
+
function clampRandom(value) {
|
|
25262
|
+
if (!Number.isFinite(value)) {
|
|
25263
|
+
return 0;
|
|
25264
|
+
}
|
|
25265
|
+
return Math.min(1, Math.max(0, value));
|
|
25266
|
+
}
|
|
25267
|
+
|
|
25268
|
+
// src/relay/stream-policy.ts
|
|
25269
|
+
var DEFAULT_RELAY_STREAM_BATCH_POLICY = {
|
|
25270
|
+
flushIntervalMs: 1e3,
|
|
25271
|
+
flushBytes: 4 * 1024
|
|
25272
|
+
};
|
|
25273
|
+
var RELAY_STREAM_POLICY_CONSTRAINTS = {
|
|
25274
|
+
flushIntervalMs: {
|
|
25275
|
+
min: 50,
|
|
25276
|
+
max: 1e3
|
|
25277
|
+
},
|
|
25278
|
+
flushBytes: {
|
|
25279
|
+
min: 1024,
|
|
25280
|
+
max: 64 * 1024
|
|
25281
|
+
}
|
|
25282
|
+
};
|
|
25283
|
+
async function fetchRelayStreamBatchPolicy(serverBaseUrl, options = {}) {
|
|
25284
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
25285
|
+
const controller = new AbortController();
|
|
25286
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
25287
|
+
timeout.unref?.();
|
|
25288
|
+
try {
|
|
25289
|
+
const response = await fetchImpl(`${serverBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/stream-policy`, {
|
|
25290
|
+
headers: {
|
|
25291
|
+
accept: "application/json"
|
|
25292
|
+
},
|
|
24038
25293
|
signal: controller.signal
|
|
24039
25294
|
});
|
|
24040
25295
|
if (!response.ok) {
|
|
@@ -24080,23 +25335,76 @@ function connectRelayControl(options) {
|
|
|
24080
25335
|
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
24081
25336
|
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
24082
25337
|
wsUrl.searchParams.set("link_id", options.linkId);
|
|
24083
|
-
const
|
|
24084
|
-
const
|
|
24085
|
-
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;
|
|
24086
25342
|
let reconnectAttempts = 0;
|
|
24087
25343
|
let closedByUser = false;
|
|
24088
25344
|
let socket = null;
|
|
24089
25345
|
let retryTimer = null;
|
|
24090
25346
|
let abortControllers = /* @__PURE__ */ new Map();
|
|
24091
25347
|
let fatalRelayRejection = null;
|
|
25348
|
+
let relayRetryAfterMs = null;
|
|
24092
25349
|
let latestNetworkRoutes = null;
|
|
24093
25350
|
const streamBatchPolicy = {
|
|
24094
25351
|
current: options.initialStreamBatchPolicy ?? DEFAULT_RELAY_STREAM_BATCH_POLICY,
|
|
24095
25352
|
onUpdate: options.onStreamBatchPolicy
|
|
24096
25353
|
};
|
|
25354
|
+
const startConnect = () => {
|
|
25355
|
+
void waitForPersistedCooldown().then((delay3) => {
|
|
25356
|
+
if (closedByUser) {
|
|
25357
|
+
return;
|
|
25358
|
+
}
|
|
25359
|
+
if (delay3 > 0) {
|
|
25360
|
+
scheduleTimer(delay3, "cooldown", `Relay reconnect cooldown active for ${delay3}ms`);
|
|
25361
|
+
return;
|
|
25362
|
+
}
|
|
25363
|
+
connect();
|
|
25364
|
+
}).catch((error) => {
|
|
25365
|
+
if (closedByUser) {
|
|
25366
|
+
return;
|
|
25367
|
+
}
|
|
25368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25369
|
+
scheduleTimer(backoffBaseMs, "retrying", `Relay connect setup failed: ${message}`);
|
|
25370
|
+
});
|
|
25371
|
+
};
|
|
24097
25372
|
const connect = () => {
|
|
24098
25373
|
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
24099
25374
|
fatalRelayRejection = null;
|
|
25375
|
+
relayRetryAfterMs = null;
|
|
25376
|
+
let closeHandled = false;
|
|
25377
|
+
const handleConnectionClosed = (reason) => {
|
|
25378
|
+
if (closeHandled) {
|
|
25379
|
+
return;
|
|
25380
|
+
}
|
|
25381
|
+
closeHandled = true;
|
|
25382
|
+
abortAll(abortControllers);
|
|
25383
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
25384
|
+
if (fatalRelayRejection) {
|
|
25385
|
+
options.onStatus?.({
|
|
25386
|
+
state: "failed",
|
|
25387
|
+
attempt: reconnectAttempts,
|
|
25388
|
+
message: fatalRelayRejection
|
|
25389
|
+
});
|
|
25390
|
+
return;
|
|
25391
|
+
}
|
|
25392
|
+
if (closedByUser) {
|
|
25393
|
+
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
25394
|
+
return;
|
|
25395
|
+
}
|
|
25396
|
+
if (Number.isFinite(maxReconnectAttempts) && reconnectAttempts >= maxReconnectAttempts) {
|
|
25397
|
+
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
25398
|
+
return;
|
|
25399
|
+
}
|
|
25400
|
+
void scheduleReconnect(reason).catch((error) => {
|
|
25401
|
+
if (closedByUser) {
|
|
25402
|
+
return;
|
|
25403
|
+
}
|
|
25404
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25405
|
+
scheduleTimer(backoffMaxMs, "retrying", `Relay reconnect scheduling failed: ${message}`);
|
|
25406
|
+
});
|
|
25407
|
+
};
|
|
24100
25408
|
socket = new WebSocket(wsUrl, {
|
|
24101
25409
|
headers: {
|
|
24102
25410
|
"x-hermes-link-version": LINK_VERSION
|
|
@@ -24104,6 +25412,7 @@ function connectRelayControl(options) {
|
|
|
24104
25412
|
});
|
|
24105
25413
|
socket.on("open", () => {
|
|
24106
25414
|
reconnectAttempts = 0;
|
|
25415
|
+
void clearRelayReconnectState(paths).catch(() => void 0);
|
|
24107
25416
|
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
24108
25417
|
const currentSocket = socket;
|
|
24109
25418
|
if (currentSocket && latestNetworkRoutes) {
|
|
@@ -24119,6 +25428,20 @@ function connectRelayControl(options) {
|
|
|
24119
25428
|
socket?.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
24120
25429
|
});
|
|
24121
25430
|
});
|
|
25431
|
+
socket.on("unexpected-response", (request, response) => {
|
|
25432
|
+
const statusCode = response.statusCode ?? 0;
|
|
25433
|
+
fatalRelayRejection = resolveFatalRelayRejectionFromStatus(statusCode);
|
|
25434
|
+
relayRetryAfterMs = readRetryAfterMs(response);
|
|
25435
|
+
const message = fatalRelayRejection ?? `Relay returned HTTP ${statusCode || "unknown"}`;
|
|
25436
|
+
options.onStatus?.({
|
|
25437
|
+
state: "disconnected",
|
|
25438
|
+
attempt: reconnectAttempts,
|
|
25439
|
+
message
|
|
25440
|
+
});
|
|
25441
|
+
response.resume();
|
|
25442
|
+
handleConnectionClosed(message);
|
|
25443
|
+
request.destroy();
|
|
25444
|
+
});
|
|
24122
25445
|
socket.on("error", (error) => {
|
|
24123
25446
|
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
24124
25447
|
fatalRelayRejection = resolveFatalRelayRejection(message);
|
|
@@ -24129,32 +25452,40 @@ function connectRelayControl(options) {
|
|
|
24129
25452
|
});
|
|
24130
25453
|
});
|
|
24131
25454
|
socket.on("close", () => {
|
|
24132
|
-
|
|
24133
|
-
abortControllers = /* @__PURE__ */ new Map();
|
|
24134
|
-
if (fatalRelayRejection) {
|
|
24135
|
-
options.onStatus?.({
|
|
24136
|
-
state: "failed",
|
|
24137
|
-
attempt: reconnectAttempts,
|
|
24138
|
-
message: fatalRelayRejection
|
|
24139
|
-
});
|
|
24140
|
-
return;
|
|
24141
|
-
}
|
|
24142
|
-
if (closedByUser) {
|
|
24143
|
-
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts });
|
|
24144
|
-
return;
|
|
24145
|
-
}
|
|
24146
|
-
if (reconnectAttempts >= maxReconnectAttempts) {
|
|
24147
|
-
options.onStatus?.({ state: "failed", attempt: reconnectAttempts, message: "Relay reconnect attempts exhausted" });
|
|
24148
|
-
return;
|
|
24149
|
-
}
|
|
24150
|
-
reconnectAttempts += 1;
|
|
24151
|
-
const delay3 = computeBackoffMs(reconnectAttempts, backoffBaseMs, backoffMaxMs);
|
|
24152
|
-
options.onStatus?.({ state: "retrying", attempt: reconnectAttempts, message: `Retrying in ${delay3}ms` });
|
|
24153
|
-
retryTimer = setTimeout(connect, delay3);
|
|
24154
|
-
retryTimer.unref?.();
|
|
25455
|
+
handleConnectionClosed();
|
|
24155
25456
|
});
|
|
24156
25457
|
};
|
|
24157
|
-
|
|
25458
|
+
startConnect();
|
|
25459
|
+
async function scheduleReconnect(reason) {
|
|
25460
|
+
const recorded = await recordRelayDisconnect(paths, { reason }).catch(() => ({
|
|
25461
|
+
disconnectCount: 0,
|
|
25462
|
+
cooldownUntilMs: null
|
|
25463
|
+
}));
|
|
25464
|
+
if (closedByUser) {
|
|
25465
|
+
return;
|
|
25466
|
+
}
|
|
25467
|
+
if (recorded.cooldownUntilMs !== null) {
|
|
25468
|
+
reconnectAttempts = 0;
|
|
25469
|
+
const delay4 = Math.max(0, recorded.cooldownUntilMs - Date.now());
|
|
25470
|
+
scheduleTimer(delay4, "cooldown", `Relay reconnect storm guard active for ${delay4}ms`);
|
|
25471
|
+
return;
|
|
25472
|
+
}
|
|
25473
|
+
reconnectAttempts += 1;
|
|
25474
|
+
const backoffMs = computeRelayBackoffMs(reconnectAttempts, {
|
|
25475
|
+
baseMs: backoffBaseMs,
|
|
25476
|
+
maxMs: backoffMaxMs
|
|
25477
|
+
});
|
|
25478
|
+
const delay3 = Math.max(backoffMs, relayRetryAfterMs ?? 0);
|
|
25479
|
+
scheduleTimer(delay3, "retrying", `Retrying in ${delay3}ms`);
|
|
25480
|
+
}
|
|
25481
|
+
async function waitForPersistedCooldown() {
|
|
25482
|
+
return await readRelayCooldownDelayMs(paths).catch(() => 0);
|
|
25483
|
+
}
|
|
25484
|
+
function scheduleTimer(delay3, state, message) {
|
|
25485
|
+
options.onStatus?.({ state, attempt: reconnectAttempts, message });
|
|
25486
|
+
retryTimer = setTimeout(connect, delay3);
|
|
25487
|
+
retryTimer.unref?.();
|
|
25488
|
+
}
|
|
24158
25489
|
return {
|
|
24159
25490
|
publishNetworkRoutes(routes) {
|
|
24160
25491
|
latestNetworkRoutes = routes;
|
|
@@ -24191,7 +25522,12 @@ function sendNetworkRoutes(socket, linkId, routes) {
|
|
|
24191
25522
|
}));
|
|
24192
25523
|
}
|
|
24193
25524
|
function resolveFatalRelayRejection(message) {
|
|
24194
|
-
|
|
25525
|
+
const match = /Unexpected server response:\s*(\d{3})\b/u.exec(message);
|
|
25526
|
+
const statusCode = match ? Number.parseInt(match[1], 10) : Number.NaN;
|
|
25527
|
+
return resolveFatalRelayRejectionFromStatus(statusCode);
|
|
25528
|
+
}
|
|
25529
|
+
function resolveFatalRelayRejectionFromStatus(statusCode) {
|
|
25530
|
+
if (!Number.isFinite(statusCode) || ![400, 401, 403, 410, 426].includes(statusCode)) {
|
|
24195
25531
|
return null;
|
|
24196
25532
|
}
|
|
24197
25533
|
return "Relay refused the Hermes Link connection. Check Link version and pairing state before retrying.";
|
|
@@ -24202,10 +25538,21 @@ function abortAll(abortControllers) {
|
|
|
24202
25538
|
}
|
|
24203
25539
|
abortControllers.clear();
|
|
24204
25540
|
}
|
|
24205
|
-
function
|
|
24206
|
-
const
|
|
24207
|
-
const
|
|
24208
|
-
|
|
25541
|
+
function readRetryAfterMs(response) {
|
|
25542
|
+
const raw = response.headers["retry-after"];
|
|
25543
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
25544
|
+
if (!value) {
|
|
25545
|
+
return null;
|
|
25546
|
+
}
|
|
25547
|
+
const seconds = Number.parseInt(value, 10);
|
|
25548
|
+
if (Number.isInteger(seconds) && seconds >= 0) {
|
|
25549
|
+
return seconds * 1e3;
|
|
25550
|
+
}
|
|
25551
|
+
const dateMs = Date.parse(value);
|
|
25552
|
+
if (!Number.isFinite(dateMs)) {
|
|
25553
|
+
return null;
|
|
25554
|
+
}
|
|
25555
|
+
return Math.max(0, dateMs - Date.now());
|
|
24209
25556
|
}
|
|
24210
25557
|
async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
|
|
24211
25558
|
const frame = JSON.parse(raw);
|
|
@@ -24326,10 +25673,58 @@ function createRelayStreamChunkBatcher(socket, id, streamBatchPolicy) {
|
|
|
24326
25673
|
};
|
|
24327
25674
|
}
|
|
24328
25675
|
|
|
25676
|
+
// src/relay/status-state.ts
|
|
25677
|
+
async function readRelayStatusSnapshot(paths) {
|
|
25678
|
+
const state = await readLinkState2(paths);
|
|
25679
|
+
return normalizeRelayStatusSnapshot(state.relayStatus);
|
|
25680
|
+
}
|
|
25681
|
+
async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new Date()) {
|
|
25682
|
+
const current = await readLinkState2(paths);
|
|
25683
|
+
await writeJsonFile(paths.stateFile, {
|
|
25684
|
+
...current,
|
|
25685
|
+
relayStatus: {
|
|
25686
|
+
state: status.state,
|
|
25687
|
+
attempt: Number.isFinite(status.attempt) ? Math.max(0, Math.floor(status.attempt)) : 0,
|
|
25688
|
+
message: normalizeMessage(status.message),
|
|
25689
|
+
updatedAt: now.toISOString()
|
|
25690
|
+
}
|
|
25691
|
+
});
|
|
25692
|
+
}
|
|
25693
|
+
async function readLinkState2(paths) {
|
|
25694
|
+
const state = await readJsonFile(paths.stateFile);
|
|
25695
|
+
return state && typeof state === "object" ? state : {};
|
|
25696
|
+
}
|
|
25697
|
+
function normalizeRelayStatusSnapshot(value) {
|
|
25698
|
+
const record = value && typeof value === "object" ? value : null;
|
|
25699
|
+
if (!record || !isRelayStatusState(record.state)) {
|
|
25700
|
+
return null;
|
|
25701
|
+
}
|
|
25702
|
+
const updatedAt = typeof record.updatedAt === "string" && Number.isFinite(Date.parse(record.updatedAt)) ? record.updatedAt : null;
|
|
25703
|
+
if (!updatedAt) {
|
|
25704
|
+
return null;
|
|
25705
|
+
}
|
|
25706
|
+
return {
|
|
25707
|
+
state: record.state,
|
|
25708
|
+
attempt: typeof record.attempt === "number" && Number.isFinite(record.attempt) ? Math.max(0, Math.floor(record.attempt)) : 0,
|
|
25709
|
+
message: normalizeMessage(record.message),
|
|
25710
|
+
updatedAt
|
|
25711
|
+
};
|
|
25712
|
+
}
|
|
25713
|
+
function isRelayStatusState(value) {
|
|
25714
|
+
return value === "connecting" || value === "connected" || value === "disconnected" || value === "retrying" || value === "cooldown" || value === "failed";
|
|
25715
|
+
}
|
|
25716
|
+
function normalizeMessage(value) {
|
|
25717
|
+
if (typeof value !== "string") {
|
|
25718
|
+
return null;
|
|
25719
|
+
}
|
|
25720
|
+
const trimmed = value.trim();
|
|
25721
|
+
return trimmed ? trimmed.slice(0, 240) : null;
|
|
25722
|
+
}
|
|
25723
|
+
|
|
24329
25724
|
// src/runtime/system-info.ts
|
|
24330
25725
|
import { execFileSync } from "child_process";
|
|
24331
25726
|
import { readFileSync } from "fs";
|
|
24332
|
-
import
|
|
25727
|
+
import os5 from "os";
|
|
24333
25728
|
function readLinkSystemInfo() {
|
|
24334
25729
|
const platform = process.platform;
|
|
24335
25730
|
const hostname = readHostname(platform);
|
|
@@ -24368,7 +25763,7 @@ function readHostname(platform) {
|
|
|
24368
25763
|
return computerName;
|
|
24369
25764
|
}
|
|
24370
25765
|
}
|
|
24371
|
-
return normalizeText(
|
|
25766
|
+
return normalizeText(os5.hostname());
|
|
24372
25767
|
}
|
|
24373
25768
|
function readOsLabel(platform) {
|
|
24374
25769
|
if (platform === "darwin") {
|
|
@@ -24376,12 +25771,12 @@ function readOsLabel(platform) {
|
|
|
24376
25771
|
return version ? `macOS ${version}` : "macOS";
|
|
24377
25772
|
}
|
|
24378
25773
|
if (platform === "linux") {
|
|
24379
|
-
return readLinuxOsRelease() ?? `Linux ${
|
|
25774
|
+
return readLinuxOsRelease() ?? `Linux ${os5.release()}`;
|
|
24380
25775
|
}
|
|
24381
25776
|
if (platform === "win32") {
|
|
24382
|
-
return `Windows ${
|
|
25777
|
+
return `Windows ${os5.release()}`;
|
|
24383
25778
|
}
|
|
24384
|
-
return `${
|
|
25779
|
+
return `${os5.type()} ${os5.release()}`.trim();
|
|
24385
25780
|
}
|
|
24386
25781
|
function readLinuxOsRelease() {
|
|
24387
25782
|
for (const file of ["/etc/os-release", "/usr/lib/os-release"]) {
|
|
@@ -24428,11 +25823,11 @@ function truncateText(value, maxLength) {
|
|
|
24428
25823
|
}
|
|
24429
25824
|
|
|
24430
25825
|
// src/topology/network.ts
|
|
24431
|
-
import
|
|
25826
|
+
import os7 from "os";
|
|
24432
25827
|
|
|
24433
25828
|
// src/topology/environment.ts
|
|
24434
25829
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
24435
|
-
import
|
|
25830
|
+
import os6 from "os";
|
|
24436
25831
|
function detectRuntimeEnvironment(env = process.env) {
|
|
24437
25832
|
if (isWsl(env)) {
|
|
24438
25833
|
return {
|
|
@@ -24461,7 +25856,7 @@ function isWsl(env) {
|
|
|
24461
25856
|
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
24462
25857
|
return true;
|
|
24463
25858
|
}
|
|
24464
|
-
const release =
|
|
25859
|
+
const release = os6.release().toLowerCase();
|
|
24465
25860
|
return release.includes("microsoft") || release.includes("wsl");
|
|
24466
25861
|
}
|
|
24467
25862
|
function isContainer(env) {
|
|
@@ -24506,7 +25901,7 @@ async function discoverRouteCandidates(options) {
|
|
|
24506
25901
|
};
|
|
24507
25902
|
}
|
|
24508
25903
|
function discoverLanIps() {
|
|
24509
|
-
return discoverLanIpsFromInterfaces(
|
|
25904
|
+
return discoverLanIpsFromInterfaces(os7.networkInterfaces());
|
|
24510
25905
|
}
|
|
24511
25906
|
function discoverLanIpsFromInterfaces(interfaces) {
|
|
24512
25907
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -24645,7 +26040,7 @@ function unique(values) {
|
|
|
24645
26040
|
// src/link/network-report-state.ts
|
|
24646
26041
|
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
24647
26042
|
async function readNetworkReportState(paths) {
|
|
24648
|
-
const state = await
|
|
26043
|
+
const state = await readLinkState3(paths);
|
|
24649
26044
|
return normalizeNetworkReportState(state.networkReport);
|
|
24650
26045
|
}
|
|
24651
26046
|
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
@@ -24712,14 +26107,14 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
|
24712
26107
|
};
|
|
24713
26108
|
}
|
|
24714
26109
|
async function updateNetworkReportState(paths, update) {
|
|
24715
|
-
const state = await
|
|
26110
|
+
const state = await readLinkState3(paths);
|
|
24716
26111
|
const next = {
|
|
24717
26112
|
...state,
|
|
24718
26113
|
networkReport: update(normalizeNetworkReportState(state.networkReport))
|
|
24719
26114
|
};
|
|
24720
26115
|
await writeJsonFile(paths.stateFile, next);
|
|
24721
26116
|
}
|
|
24722
|
-
async function
|
|
26117
|
+
async function readLinkState3(paths) {
|
|
24723
26118
|
const state = await readJsonFile(paths.stateFile);
|
|
24724
26119
|
return state && typeof state === "object" ? state : {};
|
|
24725
26120
|
}
|
|
@@ -24992,6 +26387,89 @@ async function checkLanIpChange(options, context = {}) {
|
|
|
24992
26387
|
}
|
|
24993
26388
|
}
|
|
24994
26389
|
|
|
26390
|
+
// src/daemon/process-guard.ts
|
|
26391
|
+
var installed = false;
|
|
26392
|
+
var fatalShutdownInProgress = false;
|
|
26393
|
+
var activeLogger = null;
|
|
26394
|
+
var activeOptions = {};
|
|
26395
|
+
var DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
26396
|
+
function installDaemonProcessGuard(logger, options = {}) {
|
|
26397
|
+
activeLogger = logger;
|
|
26398
|
+
activeOptions = options;
|
|
26399
|
+
if (installed) {
|
|
26400
|
+
return;
|
|
26401
|
+
}
|
|
26402
|
+
installed = true;
|
|
26403
|
+
process.on("unhandledRejection", (reason) => {
|
|
26404
|
+
void handleFatalProcessFailure("process_unhandled_rejection", reason);
|
|
26405
|
+
});
|
|
26406
|
+
process.on("uncaughtException", (error) => {
|
|
26407
|
+
void handleFatalProcessFailure("process_uncaught_exception", error);
|
|
26408
|
+
});
|
|
26409
|
+
}
|
|
26410
|
+
async function handleFatalProcessFailure(event, error) {
|
|
26411
|
+
const fields = describeProcessFailure(error);
|
|
26412
|
+
const logger = activeLogger;
|
|
26413
|
+
const options = activeOptions;
|
|
26414
|
+
if (fatalShutdownInProgress) {
|
|
26415
|
+
writeFatalFailureToStderr(event, fields);
|
|
26416
|
+
return;
|
|
26417
|
+
}
|
|
26418
|
+
fatalShutdownInProgress = true;
|
|
26419
|
+
if (logger) {
|
|
26420
|
+
try {
|
|
26421
|
+
await logger.error(event, fields);
|
|
26422
|
+
await logger.flush();
|
|
26423
|
+
} catch {
|
|
26424
|
+
}
|
|
26425
|
+
}
|
|
26426
|
+
writeFatalFailureToStderr(event, fields);
|
|
26427
|
+
if (options.onFatal) {
|
|
26428
|
+
try {
|
|
26429
|
+
await Promise.race([
|
|
26430
|
+
options.onFatal(),
|
|
26431
|
+
wait(options.shutdownTimeoutMs ?? DEFAULT_FATAL_SHUTDOWN_TIMEOUT_MS)
|
|
26432
|
+
]);
|
|
26433
|
+
} catch (shutdownError) {
|
|
26434
|
+
if (logger) {
|
|
26435
|
+
try {
|
|
26436
|
+
await logger.error("process_fatal_shutdown_failed", {
|
|
26437
|
+
error: shutdownError instanceof Error ? shutdownError.message : String(shutdownError)
|
|
26438
|
+
});
|
|
26439
|
+
await logger.flush();
|
|
26440
|
+
} catch {
|
|
26441
|
+
}
|
|
26442
|
+
}
|
|
26443
|
+
}
|
|
26444
|
+
}
|
|
26445
|
+
process.exit(1);
|
|
26446
|
+
}
|
|
26447
|
+
function describeProcessFailure(error) {
|
|
26448
|
+
if (error instanceof Error) {
|
|
26449
|
+
return {
|
|
26450
|
+
message: error.message,
|
|
26451
|
+
...error.stack ? { stack: error.stack } : {}
|
|
26452
|
+
};
|
|
26453
|
+
}
|
|
26454
|
+
return { message: String(error) };
|
|
26455
|
+
}
|
|
26456
|
+
function writeFatalFailureToStderr(event, fields) {
|
|
26457
|
+
try {
|
|
26458
|
+
process.stderr.write(
|
|
26459
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${fields.message}
|
|
26460
|
+
${fields.stack ?? ""}
|
|
26461
|
+
`
|
|
26462
|
+
);
|
|
26463
|
+
} catch {
|
|
26464
|
+
}
|
|
26465
|
+
}
|
|
26466
|
+
function wait(ms) {
|
|
26467
|
+
return new Promise((resolve) => {
|
|
26468
|
+
const timer = setTimeout(resolve, ms);
|
|
26469
|
+
timer.unref?.();
|
|
26470
|
+
});
|
|
26471
|
+
}
|
|
26472
|
+
|
|
24995
26473
|
// src/daemon/scheduler.ts
|
|
24996
26474
|
function startCronDeliveryScheduler(options) {
|
|
24997
26475
|
let running = false;
|
|
@@ -25077,6 +26555,11 @@ async function startLinkService(options = {}) {
|
|
|
25077
26555
|
current_version: migration.currentVersion
|
|
25078
26556
|
});
|
|
25079
26557
|
}
|
|
26558
|
+
await ensureHermesLinkSkillInstalledBestEffort({
|
|
26559
|
+
paths,
|
|
26560
|
+
logger,
|
|
26561
|
+
source: "service_startup"
|
|
26562
|
+
});
|
|
25080
26563
|
const conversations = new ConversationService(paths, logger);
|
|
25081
26564
|
await conversations.rebuildStatisticsIndex();
|
|
25082
26565
|
let relay = null;
|
|
@@ -25108,6 +26591,11 @@ async function startLinkService(options = {}) {
|
|
|
25108
26591
|
logger,
|
|
25109
26592
|
conversations,
|
|
25110
26593
|
onPairingClaimed: async () => {
|
|
26594
|
+
void ensureHermesLinkSkillInstalledBestEffort({
|
|
26595
|
+
paths,
|
|
26596
|
+
logger,
|
|
26597
|
+
source: "pairing_claimed"
|
|
26598
|
+
});
|
|
25111
26599
|
triggerHermesSessionSync();
|
|
25112
26600
|
void loadRelayStreamBatchPolicy("pairing_claimed");
|
|
25113
26601
|
await options.onPairingClaimed?.();
|
|
@@ -25132,6 +26620,38 @@ async function startLinkService(options = {}) {
|
|
|
25132
26620
|
error: error.message
|
|
25133
26621
|
});
|
|
25134
26622
|
});
|
|
26623
|
+
server.on("clientError", (error, socket) => {
|
|
26624
|
+
if (isExpectedClientDisconnectError3(error, {
|
|
26625
|
+
sse: isActiveSseSocket(socket)
|
|
26626
|
+
})) {
|
|
26627
|
+
socket.destroy();
|
|
26628
|
+
return;
|
|
26629
|
+
}
|
|
26630
|
+
void logger.warn("client_connection_error", {
|
|
26631
|
+
port: config.port,
|
|
26632
|
+
link_id: identity?.link_id ?? null,
|
|
26633
|
+
error: error.message
|
|
26634
|
+
});
|
|
26635
|
+
if (socket.writable) {
|
|
26636
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
26637
|
+
} else {
|
|
26638
|
+
socket.destroy();
|
|
26639
|
+
}
|
|
26640
|
+
});
|
|
26641
|
+
server.on("connection", (socket) => {
|
|
26642
|
+
socket.on("error", (error) => {
|
|
26643
|
+
if (isExpectedClientDisconnectError3(error, {
|
|
26644
|
+
sse: isActiveSseSocket(socket)
|
|
26645
|
+
})) {
|
|
26646
|
+
return;
|
|
26647
|
+
}
|
|
26648
|
+
void logger.warn("socket_error", {
|
|
26649
|
+
port: config.port,
|
|
26650
|
+
link_id: identity?.link_id ?? null,
|
|
26651
|
+
error: error.message
|
|
26652
|
+
});
|
|
26653
|
+
});
|
|
26654
|
+
});
|
|
25135
26655
|
void logger.info("service_started", {
|
|
25136
26656
|
port: config.port,
|
|
25137
26657
|
link_id: identity?.link_id ?? null
|
|
@@ -25157,9 +26677,8 @@ async function startLinkService(options = {}) {
|
|
|
25157
26677
|
relayBaseUrl: config.relayBaseUrl,
|
|
25158
26678
|
linkId: identity.link_id,
|
|
25159
26679
|
localPort: config.port,
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
backoffMaxMs: 3e4,
|
|
26680
|
+
paths,
|
|
26681
|
+
maxReconnectAttempts: options.relayMaxReconnectAttempts,
|
|
25163
26682
|
onStreamBatchPolicy: (policy) => {
|
|
25164
26683
|
void logger.info("relay_stream_policy_updated", {
|
|
25165
26684
|
flushIntervalMs: policy.flushIntervalMs,
|
|
@@ -25167,6 +26686,7 @@ async function startLinkService(options = {}) {
|
|
|
25167
26686
|
});
|
|
25168
26687
|
},
|
|
25169
26688
|
onStatus: (status) => {
|
|
26689
|
+
void writeRelayStatusSnapshot(paths, status).catch(() => void 0);
|
|
25170
26690
|
void logger.info("relay_status", status);
|
|
25171
26691
|
if (status.state === "connected") {
|
|
25172
26692
|
const now = Date.now();
|
|
@@ -25207,8 +26727,13 @@ async function startLinkService(options = {}) {
|
|
|
25207
26727
|
if (options.writePidFile) {
|
|
25208
26728
|
await writePidFile(paths);
|
|
25209
26729
|
}
|
|
25210
|
-
|
|
26730
|
+
let closed = false;
|
|
26731
|
+
const service = {
|
|
25211
26732
|
async close() {
|
|
26733
|
+
if (closed) {
|
|
26734
|
+
return;
|
|
26735
|
+
}
|
|
26736
|
+
closed = true;
|
|
25212
26737
|
relay?.close();
|
|
25213
26738
|
await closeServer(server);
|
|
25214
26739
|
await Promise.all([
|
|
@@ -25224,6 +26749,12 @@ async function startLinkService(options = {}) {
|
|
|
25224
26749
|
}
|
|
25225
26750
|
}
|
|
25226
26751
|
};
|
|
26752
|
+
if (options.writePidFile) {
|
|
26753
|
+
installDaemonProcessGuard(logger, {
|
|
26754
|
+
onFatal: () => service.close()
|
|
26755
|
+
});
|
|
26756
|
+
}
|
|
26757
|
+
return service;
|
|
25227
26758
|
}
|
|
25228
26759
|
function waitForRelayReadyTimeout(timeoutMs) {
|
|
25229
26760
|
return new Promise((resolve) => {
|
|
@@ -25278,6 +26809,16 @@ async function closeServer(server) {
|
|
|
25278
26809
|
server.closeIdleConnections?.();
|
|
25279
26810
|
});
|
|
25280
26811
|
}
|
|
26812
|
+
function isExpectedClientDisconnectError3(error, options = {}) {
|
|
26813
|
+
if (!(error instanceof Error)) {
|
|
26814
|
+
return false;
|
|
26815
|
+
}
|
|
26816
|
+
const code = String(error.code ?? "");
|
|
26817
|
+
if (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED" || /(?:socket hang up|aborted)/iu.test(error.message)) {
|
|
26818
|
+
return true;
|
|
26819
|
+
}
|
|
26820
|
+
return options.sse === true && (code === "ETIMEDOUT" || /\b(?:read\s+)?ETIMEDOUT\b/iu.test(error.message));
|
|
26821
|
+
}
|
|
25281
26822
|
async function listenServer(server, port) {
|
|
25282
26823
|
await new Promise((resolve, reject) => {
|
|
25283
26824
|
const cleanup = () => {
|
|
@@ -25299,6 +26840,16 @@ async function listenServer(server, port) {
|
|
|
25299
26840
|
}
|
|
25300
26841
|
|
|
25301
26842
|
// src/daemon/process.ts
|
|
26843
|
+
var SUPERVISOR_RESTART_INITIAL_DELAY_MS = 1e3;
|
|
26844
|
+
var SUPERVISOR_RESTART_MAX_DELAY_MS = 3e4;
|
|
26845
|
+
var SUPERVISOR_STABLE_UPTIME_MS = 6e4;
|
|
26846
|
+
var SUPERVISOR_HEALTH_STARTUP_GRACE_MS = 15e3;
|
|
26847
|
+
var SUPERVISOR_HEALTH_INTERVAL_MS = 15e3;
|
|
26848
|
+
var SUPERVISOR_HEALTH_TIMEOUT_MS = 3e3;
|
|
26849
|
+
var SUPERVISOR_HEALTH_FAILURE_THRESHOLD = 3;
|
|
26850
|
+
var SUPERVISOR_CHILD_STOP_TIMEOUT_MS = 5e3;
|
|
26851
|
+
var SUPERVISOR_STOP_INTENT_TTL_MS = 2 * 6e4;
|
|
26852
|
+
var INTERNAL_HEALTH_PROBE_HEADER2 = "x-hermes-link-internal-health-probe";
|
|
25302
26853
|
async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
25303
26854
|
const config = await loadConfig(paths);
|
|
25304
26855
|
let status = await getDaemonStatus(paths);
|
|
@@ -25323,7 +26874,7 @@ async function startDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25323
26874
|
});
|
|
25324
26875
|
child.unref();
|
|
25325
26876
|
for (let index = 0; index < 12; index += 1) {
|
|
25326
|
-
await
|
|
26877
|
+
await wait2(250);
|
|
25327
26878
|
const next = await getDaemonStatus(paths);
|
|
25328
26879
|
if (next.running && (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable) {
|
|
25329
26880
|
return next;
|
|
@@ -25335,43 +26886,92 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
|
|
|
25335
26886
|
await mkdir14(paths.logsDir, { recursive: true, mode: 448 });
|
|
25336
26887
|
const log = createRotatingTextLogWriter({
|
|
25337
26888
|
paths,
|
|
25338
|
-
fileName:
|
|
26889
|
+
fileName: path26.basename(daemonLogFile(paths))
|
|
25339
26890
|
});
|
|
25340
26891
|
const scriptPath = currentCliScriptPath();
|
|
25341
|
-
const child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
25342
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
25343
|
-
env: process.env
|
|
25344
|
-
});
|
|
25345
26892
|
const write = (chunk) => {
|
|
25346
26893
|
void log.write(chunk);
|
|
25347
26894
|
};
|
|
25348
26895
|
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor started
|
|
25349
26896
|
`);
|
|
25350
|
-
|
|
25351
|
-
|
|
26897
|
+
await clearExpiredSupervisorStopIntent(paths).catch(() => void 0);
|
|
26898
|
+
const logSupervisorUnhandledRejection = (reason) => {
|
|
26899
|
+
writeSupervisorFailure(write, "supervisor_unhandled_rejection", reason);
|
|
26900
|
+
};
|
|
26901
|
+
const logSupervisorUncaughtException = (error) => {
|
|
26902
|
+
writeSupervisorFailure(write, "supervisor_uncaught_exception", error);
|
|
26903
|
+
};
|
|
26904
|
+
let child = null;
|
|
26905
|
+
let stopRequested = false;
|
|
25352
26906
|
const forwardStop = () => {
|
|
25353
|
-
|
|
26907
|
+
stopRequested = true;
|
|
26908
|
+
if (child?.pid && isProcessAlive3(child.pid)) {
|
|
25354
26909
|
child.kill("SIGTERM");
|
|
25355
26910
|
}
|
|
25356
26911
|
};
|
|
25357
26912
|
process.once("SIGINT", forwardStop);
|
|
25358
26913
|
process.once("SIGTERM", forwardStop);
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
25368
|
-
|
|
26914
|
+
process.on("unhandledRejection", logSupervisorUnhandledRejection);
|
|
26915
|
+
process.on("uncaughtException", logSupervisorUncaughtException);
|
|
26916
|
+
let restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
|
|
26917
|
+
let finalResult = {
|
|
26918
|
+
code: 0,
|
|
26919
|
+
signal: null
|
|
26920
|
+
};
|
|
26921
|
+
try {
|
|
26922
|
+
while (!stopRequested) {
|
|
26923
|
+
const startedAt = Date.now();
|
|
26924
|
+
child = spawn4(process.execPath, [scriptPath, "daemon", "--foreground"], {
|
|
26925
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26926
|
+
env: process.env
|
|
26927
|
+
});
|
|
26928
|
+
const childPid = child.pid ?? null;
|
|
26929
|
+
child.stdout?.on("data", write);
|
|
26930
|
+
child.stderr?.on("data", write);
|
|
26931
|
+
const healthMonitor = startSupervisorHealthMonitor(paths, child, write);
|
|
26932
|
+
const result = await new Promise((resolve, reject) => {
|
|
26933
|
+
child?.once("error", reject);
|
|
26934
|
+
child?.once("exit", (code, signal) => resolve({ code, signal }));
|
|
26935
|
+
}).catch((error) => {
|
|
26936
|
+
write(
|
|
26937
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child failed to start: ${error instanceof Error ? error.message : String(error)}
|
|
26938
|
+
`
|
|
26939
|
+
);
|
|
26940
|
+
return { code: 1, signal: null };
|
|
26941
|
+
});
|
|
26942
|
+
healthMonitor.close();
|
|
26943
|
+
finalResult = result;
|
|
26944
|
+
child = null;
|
|
26945
|
+
const expectedStop = childPid !== null && await consumeSupervisorStopIntent(paths, childPid);
|
|
26946
|
+
if (stopRequested || expectedStop || result.code === 0 || isIntentionalStopSignal(result.signal)) {
|
|
26947
|
+
break;
|
|
26948
|
+
}
|
|
26949
|
+
const uptimeMs = Date.now() - startedAt;
|
|
26950
|
+
if (uptimeMs >= SUPERVISOR_STABLE_UPTIME_MS) {
|
|
26951
|
+
restartDelayMs = SUPERVISOR_RESTART_INITIAL_DELAY_MS;
|
|
26952
|
+
}
|
|
26953
|
+
write(
|
|
26954
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child stopped unexpectedly code=${result.code ?? "null"} signal=${result.signal ?? "null"}; restarting in ${restartDelayMs}ms
|
|
26955
|
+
`
|
|
26956
|
+
);
|
|
26957
|
+
await wait2(restartDelayMs);
|
|
26958
|
+
restartDelayMs = Math.min(
|
|
26959
|
+
restartDelayMs * 2,
|
|
26960
|
+
SUPERVISOR_RESTART_MAX_DELAY_MS
|
|
26961
|
+
);
|
|
26962
|
+
}
|
|
26963
|
+
} finally {
|
|
26964
|
+
process.off("SIGINT", forwardStop);
|
|
26965
|
+
process.off("SIGTERM", forwardStop);
|
|
26966
|
+
process.off("unhandledRejection", logSupervisorUnhandledRejection);
|
|
26967
|
+
process.off("uncaughtException", logSupervisorUncaughtException);
|
|
26968
|
+
}
|
|
25369
26969
|
write(
|
|
25370
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${
|
|
26970
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon supervisor stopped code=${finalResult.code ?? "null"} signal=${finalResult.signal ?? "null"}
|
|
25371
26971
|
`
|
|
25372
26972
|
);
|
|
25373
26973
|
await log.flush();
|
|
25374
|
-
return
|
|
26974
|
+
return finalResult.code ?? (finalResult.signal ? 0 : 1);
|
|
25375
26975
|
}
|
|
25376
26976
|
async function probeLocalLinkService(options) {
|
|
25377
26977
|
const unreachable = {
|
|
@@ -25409,6 +27009,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25409
27009
|
if (!status.running || !status.pid) {
|
|
25410
27010
|
return status;
|
|
25411
27011
|
}
|
|
27012
|
+
await writeSupervisorStopIntent(paths, status.pid).catch(() => void 0);
|
|
25412
27013
|
try {
|
|
25413
27014
|
process.kill(status.pid, "SIGTERM");
|
|
25414
27015
|
} catch {
|
|
@@ -25416,7 +27017,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25416
27017
|
return await getDaemonStatus(paths);
|
|
25417
27018
|
}
|
|
25418
27019
|
for (let index = 0; index < 20; index += 1) {
|
|
25419
|
-
await
|
|
27020
|
+
await wait2(250);
|
|
25420
27021
|
if (!isProcessAlive3(status.pid)) {
|
|
25421
27022
|
break;
|
|
25422
27023
|
}
|
|
@@ -25427,7 +27028,7 @@ async function stopDaemonProcess(paths = resolveRuntimePaths()) {
|
|
|
25427
27028
|
} catch {
|
|
25428
27029
|
}
|
|
25429
27030
|
for (let index = 0; index < 10; index += 1) {
|
|
25430
|
-
await
|
|
27031
|
+
await wait2(250);
|
|
25431
27032
|
if (!isProcessAlive3(status.pid)) {
|
|
25432
27033
|
break;
|
|
25433
27034
|
}
|
|
@@ -25464,7 +27065,7 @@ function currentCliScriptPath() {
|
|
|
25464
27065
|
return process.argv[1];
|
|
25465
27066
|
}
|
|
25466
27067
|
async function readPid(filePath) {
|
|
25467
|
-
const raw = await
|
|
27068
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
25468
27069
|
if (!raw) {
|
|
25469
27070
|
return null;
|
|
25470
27071
|
}
|
|
@@ -25479,6 +27080,171 @@ function isProcessAlive3(pid) {
|
|
|
25479
27080
|
return false;
|
|
25480
27081
|
}
|
|
25481
27082
|
}
|
|
27083
|
+
function isIntentionalStopSignal(signal) {
|
|
27084
|
+
return signal === "SIGINT" || signal === "SIGTERM" || signal === "SIGKILL";
|
|
27085
|
+
}
|
|
27086
|
+
function startSupervisorHealthMonitor(paths, child, write) {
|
|
27087
|
+
let closed = false;
|
|
27088
|
+
let failureCount = 0;
|
|
27089
|
+
let timer = null;
|
|
27090
|
+
let forceKillTimer = null;
|
|
27091
|
+
const schedule = (delayMs) => {
|
|
27092
|
+
timer = setTimeout(check, delayMs);
|
|
27093
|
+
timer.unref?.();
|
|
27094
|
+
};
|
|
27095
|
+
const check = () => {
|
|
27096
|
+
void probeSupervisorHttpHealth(paths).then((healthy) => {
|
|
27097
|
+
if (closed) {
|
|
27098
|
+
return;
|
|
27099
|
+
}
|
|
27100
|
+
if (healthy) {
|
|
27101
|
+
failureCount = 0;
|
|
27102
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27103
|
+
return;
|
|
27104
|
+
}
|
|
27105
|
+
failureCount += 1;
|
|
27106
|
+
if (failureCount < SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
|
|
27107
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27108
|
+
return;
|
|
27109
|
+
}
|
|
27110
|
+
closed = true;
|
|
27111
|
+
write(
|
|
27112
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed ${failureCount} consecutive times; restarting child
|
|
27113
|
+
`
|
|
27114
|
+
);
|
|
27115
|
+
terminateChild(child, forceKillTimer);
|
|
27116
|
+
forceKillTimer = setTimeout(() => {
|
|
27117
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27118
|
+
child.kill("SIGKILL");
|
|
27119
|
+
}
|
|
27120
|
+
}, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
|
|
27121
|
+
forceKillTimer.unref?.();
|
|
27122
|
+
}).catch((error) => {
|
|
27123
|
+
if (closed) {
|
|
27124
|
+
return;
|
|
27125
|
+
}
|
|
27126
|
+
failureCount += 1;
|
|
27127
|
+
write(
|
|
27128
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon child health probe failed: ${error instanceof Error ? error.message : String(error)}
|
|
27129
|
+
`
|
|
27130
|
+
);
|
|
27131
|
+
if (failureCount >= SUPERVISOR_HEALTH_FAILURE_THRESHOLD) {
|
|
27132
|
+
closed = true;
|
|
27133
|
+
terminateChild(child, forceKillTimer);
|
|
27134
|
+
forceKillTimer = setTimeout(() => {
|
|
27135
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27136
|
+
child.kill("SIGKILL");
|
|
27137
|
+
}
|
|
27138
|
+
}, SUPERVISOR_CHILD_STOP_TIMEOUT_MS);
|
|
27139
|
+
forceKillTimer.unref?.();
|
|
27140
|
+
return;
|
|
27141
|
+
}
|
|
27142
|
+
schedule(SUPERVISOR_HEALTH_INTERVAL_MS);
|
|
27143
|
+
});
|
|
27144
|
+
};
|
|
27145
|
+
schedule(SUPERVISOR_HEALTH_STARTUP_GRACE_MS);
|
|
27146
|
+
return {
|
|
27147
|
+
close() {
|
|
27148
|
+
closed = true;
|
|
27149
|
+
if (timer) {
|
|
27150
|
+
clearTimeout(timer);
|
|
27151
|
+
timer = null;
|
|
27152
|
+
}
|
|
27153
|
+
if (forceKillTimer) {
|
|
27154
|
+
clearTimeout(forceKillTimer);
|
|
27155
|
+
forceKillTimer = null;
|
|
27156
|
+
}
|
|
27157
|
+
}
|
|
27158
|
+
};
|
|
27159
|
+
}
|
|
27160
|
+
async function probeSupervisorHttpHealth(paths) {
|
|
27161
|
+
const config = await loadConfig(paths).catch(() => null);
|
|
27162
|
+
if (!config) {
|
|
27163
|
+
return false;
|
|
27164
|
+
}
|
|
27165
|
+
try {
|
|
27166
|
+
const response = await fetch(`http://127.0.0.1:${config.port}/api/v1/bootstrap`, {
|
|
27167
|
+
headers: {
|
|
27168
|
+
accept: "application/json",
|
|
27169
|
+
[INTERNAL_HEALTH_PROBE_HEADER2]: "1"
|
|
27170
|
+
},
|
|
27171
|
+
signal: AbortSignal.timeout(SUPERVISOR_HEALTH_TIMEOUT_MS)
|
|
27172
|
+
});
|
|
27173
|
+
return response.ok;
|
|
27174
|
+
} catch {
|
|
27175
|
+
return false;
|
|
27176
|
+
}
|
|
27177
|
+
}
|
|
27178
|
+
function terminateChild(child, previousForceKillTimer) {
|
|
27179
|
+
if (previousForceKillTimer) {
|
|
27180
|
+
clearTimeout(previousForceKillTimer);
|
|
27181
|
+
}
|
|
27182
|
+
if (child.pid && isProcessAlive3(child.pid)) {
|
|
27183
|
+
child.kill("SIGTERM");
|
|
27184
|
+
}
|
|
27185
|
+
}
|
|
27186
|
+
function supervisorStopIntentPath(paths) {
|
|
27187
|
+
return path26.join(paths.runDir, "supervisor-stop-intent.json");
|
|
27188
|
+
}
|
|
27189
|
+
async function writeSupervisorStopIntent(paths, pid) {
|
|
27190
|
+
await mkdir14(paths.runDir, { recursive: true, mode: 448 });
|
|
27191
|
+
await writeFile4(
|
|
27192
|
+
supervisorStopIntentPath(paths),
|
|
27193
|
+
`${JSON.stringify({ pid, created_at: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
27194
|
+
`,
|
|
27195
|
+
{ mode: 384 }
|
|
27196
|
+
);
|
|
27197
|
+
}
|
|
27198
|
+
async function consumeSupervisorStopIntent(paths, pid) {
|
|
27199
|
+
const filePath = supervisorStopIntentPath(paths);
|
|
27200
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
27201
|
+
if (!raw) {
|
|
27202
|
+
return false;
|
|
27203
|
+
}
|
|
27204
|
+
const payload = parseSupervisorStopIntent(raw);
|
|
27205
|
+
if (!isValidSupervisorStopIntent(payload)) {
|
|
27206
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27207
|
+
return false;
|
|
27208
|
+
}
|
|
27209
|
+
if (payload.pid !== pid) {
|
|
27210
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27211
|
+
return false;
|
|
27212
|
+
}
|
|
27213
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27214
|
+
return true;
|
|
27215
|
+
}
|
|
27216
|
+
async function clearExpiredSupervisorStopIntent(paths) {
|
|
27217
|
+
const filePath = supervisorStopIntentPath(paths);
|
|
27218
|
+
const raw = await readFile19(filePath, "utf8").catch(() => null);
|
|
27219
|
+
if (!raw) {
|
|
27220
|
+
return;
|
|
27221
|
+
}
|
|
27222
|
+
const payload = parseSupervisorStopIntent(raw);
|
|
27223
|
+
if (!isValidSupervisorStopIntent(payload)) {
|
|
27224
|
+
await rm9(filePath, { force: true }).catch(() => void 0);
|
|
27225
|
+
}
|
|
27226
|
+
}
|
|
27227
|
+
function parseSupervisorStopIntent(raw) {
|
|
27228
|
+
try {
|
|
27229
|
+
return JSON.parse(raw);
|
|
27230
|
+
} catch {
|
|
27231
|
+
return {};
|
|
27232
|
+
}
|
|
27233
|
+
}
|
|
27234
|
+
function isValidSupervisorStopIntent(payload) {
|
|
27235
|
+
if (typeof payload.pid !== "number" || !Number.isInteger(payload.pid) || typeof payload.created_at !== "string") {
|
|
27236
|
+
return false;
|
|
27237
|
+
}
|
|
27238
|
+
const createdAtMs = Date.parse(payload.created_at);
|
|
27239
|
+
return Number.isFinite(createdAtMs) && Date.now() - createdAtMs <= SUPERVISOR_STOP_INTENT_TTL_MS;
|
|
27240
|
+
}
|
|
27241
|
+
function writeSupervisorFailure(write, event, error) {
|
|
27242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27243
|
+
const stack = error instanceof Error && error.stack ? `
|
|
27244
|
+
${error.stack}` : "";
|
|
27245
|
+
write(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${event}: ${message}${stack}
|
|
27246
|
+
`);
|
|
27247
|
+
}
|
|
25482
27248
|
async function pidBackedServiceIsReachable(paths) {
|
|
25483
27249
|
const config = await loadConfig(paths).catch(() => null);
|
|
25484
27250
|
if (!config) {
|
|
@@ -25486,7 +27252,7 @@ async function pidBackedServiceIsReachable(paths) {
|
|
|
25486
27252
|
}
|
|
25487
27253
|
return (await probeLocalLinkService({ port: config.port, timeoutMs: 500 })).reachable;
|
|
25488
27254
|
}
|
|
25489
|
-
function
|
|
27255
|
+
function wait2(ms) {
|
|
25490
27256
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25491
27257
|
}
|
|
25492
27258
|
|
|
@@ -25893,7 +27659,7 @@ async function readRemoteLinkPolicy(options) {
|
|
|
25893
27659
|
}
|
|
25894
27660
|
}
|
|
25895
27661
|
function normalizeServerSnapshot(payload) {
|
|
25896
|
-
const snapshot =
|
|
27662
|
+
const snapshot = toRecord20(payload);
|
|
25897
27663
|
const policy = toNullableRecord2(snapshot.policy);
|
|
25898
27664
|
if (!policy) {
|
|
25899
27665
|
return {
|
|
@@ -26236,7 +28002,7 @@ async function writeUpdateState2(paths, state) {
|
|
|
26236
28002
|
await writeJsonFile(updateStatePath2(paths), state);
|
|
26237
28003
|
}
|
|
26238
28004
|
async function readUpdateLogLines2(paths) {
|
|
26239
|
-
const raw = await
|
|
28005
|
+
const raw = await readFile20(updateLogPath2(paths), "utf8").catch(() => "");
|
|
26240
28006
|
if (!raw.trim()) {
|
|
26241
28007
|
return [];
|
|
26242
28008
|
}
|
|
@@ -26245,10 +28011,10 @@ async function readUpdateLogLines2(paths) {
|
|
|
26245
28011
|
);
|
|
26246
28012
|
}
|
|
26247
28013
|
function updateStatePath2(paths) {
|
|
26248
|
-
return
|
|
28014
|
+
return path27.join(paths.runDir, "link-update-state.json");
|
|
26249
28015
|
}
|
|
26250
28016
|
function updateLogPath2(paths) {
|
|
26251
|
-
return
|
|
28017
|
+
return path27.join(paths.logsDir, UPDATE_LOG_FILE2);
|
|
26252
28018
|
}
|
|
26253
28019
|
async function clearUpdateLogFiles2(paths) {
|
|
26254
28020
|
const primary = updateLogPath2(paths);
|
|
@@ -26320,7 +28086,7 @@ function isProcessAlive4(pid) {
|
|
|
26320
28086
|
return false;
|
|
26321
28087
|
}
|
|
26322
28088
|
}
|
|
26323
|
-
function
|
|
28089
|
+
function toRecord20(value) {
|
|
26324
28090
|
return typeof value === "object" && value !== null ? value : {};
|
|
26325
28091
|
}
|
|
26326
28092
|
function toNullableRecord2(value) {
|
|
@@ -26332,7 +28098,7 @@ function readString20(payload, key) {
|
|
|
26332
28098
|
}
|
|
26333
28099
|
|
|
26334
28100
|
// src/pairing/pairing.ts
|
|
26335
|
-
import
|
|
28101
|
+
import path28 from "path";
|
|
26336
28102
|
import { rm as rm11 } from "fs/promises";
|
|
26337
28103
|
|
|
26338
28104
|
// src/relay/bootstrap.ts
|
|
@@ -26672,10 +28438,10 @@ async function loadRequiredIdentity2(paths) {
|
|
|
26672
28438
|
}
|
|
26673
28439
|
return identity;
|
|
26674
28440
|
}
|
|
26675
|
-
async function postServerJson(serverBaseUrl,
|
|
28441
|
+
async function postServerJson(serverBaseUrl, path29, body, options) {
|
|
26676
28442
|
let response;
|
|
26677
28443
|
try {
|
|
26678
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
28444
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
|
|
26679
28445
|
method: "POST",
|
|
26680
28446
|
headers: {
|
|
26681
28447
|
accept: "application/json",
|
|
@@ -26723,10 +28489,10 @@ function pairingErrorSnapshot(stage, error) {
|
|
|
26723
28489
|
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
26724
28490
|
};
|
|
26725
28491
|
}
|
|
26726
|
-
async function patchServerJson(serverBaseUrl,
|
|
28492
|
+
async function patchServerJson(serverBaseUrl, path29, token, body, options) {
|
|
26727
28493
|
let response;
|
|
26728
28494
|
try {
|
|
26729
|
-
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${
|
|
28495
|
+
response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path29}`, {
|
|
26730
28496
|
method: "PATCH",
|
|
26731
28497
|
headers: {
|
|
26732
28498
|
accept: "application/json",
|
|
@@ -26774,10 +28540,10 @@ function createPairingNetworkError(input) {
|
|
|
26774
28540
|
);
|
|
26775
28541
|
}
|
|
26776
28542
|
function pairingClaimPath(sessionId, paths) {
|
|
26777
|
-
return
|
|
28543
|
+
return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
|
|
26778
28544
|
}
|
|
26779
28545
|
function pairingSessionPath(sessionId, paths) {
|
|
26780
|
-
return
|
|
28546
|
+
return path28.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
|
|
26781
28547
|
}
|
|
26782
28548
|
function qrPreferredUrls(routes) {
|
|
26783
28549
|
return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
|
|
@@ -26891,7 +28657,12 @@ function registerSystemRoutes(router, options) {
|
|
|
26891
28657
|
error: error instanceof Error ? error.message : String(error)
|
|
26892
28658
|
});
|
|
26893
28659
|
});
|
|
26894
|
-
void options.onPairingClaimed?.()
|
|
28660
|
+
void Promise.resolve().then(() => options.onPairingClaimed?.()).catch((error) => {
|
|
28661
|
+
void logger.warn("pairing_claim_callback_failed", {
|
|
28662
|
+
session_id: sessionId,
|
|
28663
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28664
|
+
});
|
|
28665
|
+
});
|
|
26895
28666
|
}, 250);
|
|
26896
28667
|
timer.unref?.();
|
|
26897
28668
|
});
|
|
@@ -27929,6 +29700,18 @@ async function createApp(options = {}) {
|
|
|
27929
29700
|
};
|
|
27930
29701
|
const app = new Koa();
|
|
27931
29702
|
const router = new Router();
|
|
29703
|
+
app.on("error", (error, ctx) => {
|
|
29704
|
+
if (isExpectedClientDisconnectError2(error, {
|
|
29705
|
+
sse: isSseRequestContext(ctx)
|
|
29706
|
+
})) {
|
|
29707
|
+
return;
|
|
29708
|
+
}
|
|
29709
|
+
void logger.error("http_app_error", {
|
|
29710
|
+
method: ctx?.method ?? null,
|
|
29711
|
+
path: ctx?.path ?? null,
|
|
29712
|
+
error: error instanceof Error ? error.message : String(error)
|
|
29713
|
+
});
|
|
29714
|
+
});
|
|
27932
29715
|
app.use(createHttpErrorMiddleware(logger));
|
|
27933
29716
|
registerSystemRoutes(router, {
|
|
27934
29717
|
paths,
|
|
@@ -27967,6 +29750,11 @@ export {
|
|
|
27967
29750
|
resolveRuntimePaths,
|
|
27968
29751
|
createFileLogger,
|
|
27969
29752
|
getLinkLogFile,
|
|
29753
|
+
readRecentLogEntries,
|
|
29754
|
+
readRecentTextLogEntries,
|
|
29755
|
+
getGatewayLogFiles,
|
|
29756
|
+
readRecentGatewayLogEntries,
|
|
29757
|
+
flushLogFiles,
|
|
27970
29758
|
ensureHermesApiServerAvailable,
|
|
27971
29759
|
readHermesVersion,
|
|
27972
29760
|
defaultLinkConfig,
|
|
@@ -27979,6 +29767,7 @@ export {
|
|
|
27979
29767
|
getIdentityStatus,
|
|
27980
29768
|
ConversationService,
|
|
27981
29769
|
hasActiveDevices,
|
|
29770
|
+
ensureHermesLinkSkillInstalledBestEffort,
|
|
27982
29771
|
detectRuntimeEnvironment,
|
|
27983
29772
|
preparePairing,
|
|
27984
29773
|
readPairingClaim,
|
|
@@ -27986,6 +29775,7 @@ export {
|
|
|
27986
29775
|
createApp,
|
|
27987
29776
|
fetchRelayStreamBatchPolicy,
|
|
27988
29777
|
connectRelayControl,
|
|
29778
|
+
readRelayStatusSnapshot,
|
|
27989
29779
|
reportLinkStatusToServer,
|
|
27990
29780
|
startLinkService,
|
|
27991
29781
|
startDaemonProcess,
|