@hermespilot/link 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PSHYSZAP.js → chunk-PEHQ3AED.js} +270 -46
- package/dist/cli/index.js +1 -1
- package/dist/http/app.d.ts +2 -0
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
|
@@ -3724,7 +3724,7 @@ import os2 from "os";
|
|
|
3724
3724
|
import path5 from "path";
|
|
3725
3725
|
|
|
3726
3726
|
// src/constants.ts
|
|
3727
|
-
var LINK_VERSION = "0.3.
|
|
3727
|
+
var LINK_VERSION = "0.3.2";
|
|
3728
3728
|
var LINK_COMMAND = "hermeslink";
|
|
3729
3729
|
var LINK_DEFAULT_PORT = 52379;
|
|
3730
3730
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
@@ -4091,7 +4091,8 @@ import { execFile } from "child_process";
|
|
|
4091
4091
|
import { promisify } from "util";
|
|
4092
4092
|
var execFileAsync = promisify(execFile);
|
|
4093
4093
|
async function deleteHermesSession(sessionId, profileName = "default") {
|
|
4094
|
-
|
|
4094
|
+
const normalizedSessionId = sessionId.trim();
|
|
4095
|
+
if (!normalizedSessionId) {
|
|
4095
4096
|
throw new LinkHttpError(
|
|
4096
4097
|
400,
|
|
4097
4098
|
"hermes_session_id_required",
|
|
@@ -4099,15 +4100,31 @@ async function deleteHermesSession(sessionId, profileName = "default") {
|
|
|
4099
4100
|
);
|
|
4100
4101
|
}
|
|
4101
4102
|
try {
|
|
4102
|
-
await execFileAsync(
|
|
4103
|
+
const output = await execFileAsync(
|
|
4103
4104
|
resolveHermesBin(),
|
|
4104
|
-
[
|
|
4105
|
+
[
|
|
4106
|
+
...profileArgs(profileName),
|
|
4107
|
+
"sessions",
|
|
4108
|
+
"delete",
|
|
4109
|
+
normalizedSessionId,
|
|
4110
|
+
"--yes"
|
|
4111
|
+
],
|
|
4105
4112
|
{
|
|
4106
4113
|
timeout: 1e4,
|
|
4107
4114
|
windowsHide: true
|
|
4108
4115
|
}
|
|
4109
4116
|
);
|
|
4117
|
+
return {
|
|
4118
|
+
session_id: normalizedSessionId,
|
|
4119
|
+
status: readSessionDeleteStatus(output.stdout, output.stderr)
|
|
4120
|
+
};
|
|
4110
4121
|
} catch (error) {
|
|
4122
|
+
if (isSessionNotFoundOutput(readExecErrorOutput(error))) {
|
|
4123
|
+
return {
|
|
4124
|
+
session_id: normalizedSessionId,
|
|
4125
|
+
status: "not_found"
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4111
4128
|
throw new LinkHttpError(
|
|
4112
4129
|
502,
|
|
4113
4130
|
"hermes_session_delete_failed",
|
|
@@ -4158,6 +4175,29 @@ async function renameHermesSession(sessionId, title, profileName = "default") {
|
|
|
4158
4175
|
function resolveHermesBin() {
|
|
4159
4176
|
return process.env.HERMES_BIN?.trim() || "hermes";
|
|
4160
4177
|
}
|
|
4178
|
+
function readSessionDeleteStatus(stdout, stderr) {
|
|
4179
|
+
const output = `${stdout.toString()}
|
|
4180
|
+
${stderr.toString()}`;
|
|
4181
|
+
if (isSessionNotFoundOutput(output)) {
|
|
4182
|
+
return "not_found";
|
|
4183
|
+
}
|
|
4184
|
+
if (/deleted session\b/i.test(output)) {
|
|
4185
|
+
return "deleted";
|
|
4186
|
+
}
|
|
4187
|
+
return "unknown";
|
|
4188
|
+
}
|
|
4189
|
+
function isSessionNotFoundOutput(output) {
|
|
4190
|
+
return /\bsession\b[\s\S]*\bnot found\b/i.test(output);
|
|
4191
|
+
}
|
|
4192
|
+
function readExecErrorOutput(error) {
|
|
4193
|
+
if (typeof error !== "object" || error === null) {
|
|
4194
|
+
return "";
|
|
4195
|
+
}
|
|
4196
|
+
const stdout = "stdout" in error && error.stdout != null ? String(error.stdout) : "";
|
|
4197
|
+
const stderr = "stderr" in error && error.stderr != null ? String(error.stderr) : "";
|
|
4198
|
+
return `${stdout}
|
|
4199
|
+
${stderr}`;
|
|
4200
|
+
}
|
|
4161
4201
|
function profileArgs(profileName) {
|
|
4162
4202
|
const normalized = profileName.trim() || "default";
|
|
4163
4203
|
return normalized === "default" ? [] : ["-p", normalized];
|
|
@@ -6327,6 +6367,51 @@ function safePathSegment(value, fallback) {
|
|
|
6327
6367
|
return safe.length > 0 ? safe.slice(0, 120) : fallback;
|
|
6328
6368
|
}
|
|
6329
6369
|
|
|
6370
|
+
// src/conversations/conversation-session-ids.ts
|
|
6371
|
+
function normalizeHermesSessionIds(values) {
|
|
6372
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6373
|
+
for (const value of values) {
|
|
6374
|
+
const sessionId = value?.trim();
|
|
6375
|
+
if (!sessionId || seen.has(sessionId)) {
|
|
6376
|
+
continue;
|
|
6377
|
+
}
|
|
6378
|
+
seen.add(sessionId);
|
|
6379
|
+
}
|
|
6380
|
+
return [...seen];
|
|
6381
|
+
}
|
|
6382
|
+
function addHermesSessionIdToManifest(manifest, sessionId) {
|
|
6383
|
+
const normalizedSessionId = sessionId.trim();
|
|
6384
|
+
if (!normalizedSessionId) {
|
|
6385
|
+
return manifest;
|
|
6386
|
+
}
|
|
6387
|
+
const hermesSessionIds = normalizeHermesSessionIds([
|
|
6388
|
+
...manifest.hermes_session_ids ?? [],
|
|
6389
|
+
manifest.hermes_session_id,
|
|
6390
|
+
normalizedSessionId
|
|
6391
|
+
]);
|
|
6392
|
+
if (manifest.hermes_session_id === normalizedSessionId && arraysEqual(manifest.hermes_session_ids ?? [], hermesSessionIds)) {
|
|
6393
|
+
return manifest;
|
|
6394
|
+
}
|
|
6395
|
+
return {
|
|
6396
|
+
...manifest,
|
|
6397
|
+
hermes_session_id: normalizedSessionId,
|
|
6398
|
+
hermes_session_ids: hermesSessionIds
|
|
6399
|
+
};
|
|
6400
|
+
}
|
|
6401
|
+
function collectHermesSessionIds(manifest, snapshot) {
|
|
6402
|
+
return normalizeHermesSessionIds([
|
|
6403
|
+
manifest.hermes_session_id,
|
|
6404
|
+
...manifest.hermes_session_ids ?? [],
|
|
6405
|
+
...snapshot.runs.map((run) => run.hermes_session_id)
|
|
6406
|
+
]);
|
|
6407
|
+
}
|
|
6408
|
+
function arraysEqual(left, right) {
|
|
6409
|
+
if (left.length !== right.length) {
|
|
6410
|
+
return false;
|
|
6411
|
+
}
|
|
6412
|
+
return left.every((item, index) => item === right[index]);
|
|
6413
|
+
}
|
|
6414
|
+
|
|
6330
6415
|
// src/conversations/conversation-maintenance.ts
|
|
6331
6416
|
var MAX_UPLOADED_BLOB_BYTES = 50 * 1024 * 1024;
|
|
6332
6417
|
var ConversationMaintenanceCoordinator = class {
|
|
@@ -6447,10 +6532,18 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
6447
6532
|
...collectBlobIds(snapshot),
|
|
6448
6533
|
...await this.listConversationBlobIds(conversationId)
|
|
6449
6534
|
]);
|
|
6450
|
-
|
|
6451
|
-
|
|
6535
|
+
const hermesSessionIds = collectHermesSessionIds(manifest, snapshot);
|
|
6536
|
+
const hermesDeleteResults = await this.deleteHermesSessions(
|
|
6537
|
+
hermesSessionIds,
|
|
6452
6538
|
manifest.profile_name_snapshot ?? manifest.profile ?? "default"
|
|
6453
6539
|
);
|
|
6540
|
+
if (snapshot.runs.length > 0 && hermesDeleteResults.every((result) => result.status === "not_found")) {
|
|
6541
|
+
throw new LinkHttpError(
|
|
6542
|
+
502,
|
|
6543
|
+
"hermes_session_delete_not_confirmed",
|
|
6544
|
+
"Hermes session deletion was not confirmed"
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6454
6547
|
const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6455
6548
|
const stats = buildConversationStats(
|
|
6456
6549
|
{
|
|
@@ -6464,6 +6557,7 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
6464
6557
|
const next = {
|
|
6465
6558
|
...manifest,
|
|
6466
6559
|
status: "deleted_soft",
|
|
6560
|
+
hermes_session_ids: hermesSessionIds,
|
|
6467
6561
|
updated_at: deletedAt,
|
|
6468
6562
|
deleted_at: deletedAt,
|
|
6469
6563
|
stats
|
|
@@ -6473,7 +6567,9 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
6473
6567
|
type: "conversation.deleted",
|
|
6474
6568
|
payload: {
|
|
6475
6569
|
deleted_at: deletedAt,
|
|
6476
|
-
hermes_session_id: manifest.hermes_session_id
|
|
6570
|
+
hermes_session_id: manifest.hermes_session_id,
|
|
6571
|
+
hermes_session_ids: hermesSessionIds,
|
|
6572
|
+
hermes_delete_results: hermesDeleteResults
|
|
6477
6573
|
}
|
|
6478
6574
|
});
|
|
6479
6575
|
await this.deps.store.writeSnapshot(conversationId, emptySnapshot2());
|
|
@@ -6491,9 +6587,17 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
6491
6587
|
return {
|
|
6492
6588
|
conversation_id: conversationId,
|
|
6493
6589
|
hermes_deleted: true,
|
|
6590
|
+
hermes_session_ids: hermesSessionIds,
|
|
6494
6591
|
deleted_at: deletedAt
|
|
6495
6592
|
};
|
|
6496
6593
|
}
|
|
6594
|
+
async deleteHermesSessions(sessionIds, profileName) {
|
|
6595
|
+
const results = [];
|
|
6596
|
+
for (const sessionId of sessionIds) {
|
|
6597
|
+
results.push(await deleteHermesSession(sessionId, profileName));
|
|
6598
|
+
}
|
|
6599
|
+
return results;
|
|
6600
|
+
}
|
|
6497
6601
|
async pruneConversationBlobReferences(conversationId, blobIds) {
|
|
6498
6602
|
for (const blobId of blobIds) {
|
|
6499
6603
|
try {
|
|
@@ -7640,8 +7744,12 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
7640
7744
|
const resetAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7641
7745
|
const previousSessionId = input.manifest.hermes_session_id;
|
|
7642
7746
|
const nextSessionId = freshHermesSessionId(input.manifest.id);
|
|
7747
|
+
const nextManifest = addHermesSessionIdToManifest(
|
|
7748
|
+
input.manifest,
|
|
7749
|
+
nextSessionId
|
|
7750
|
+
);
|
|
7643
7751
|
await this.deps.store.writeManifest({
|
|
7644
|
-
...
|
|
7752
|
+
...nextManifest,
|
|
7645
7753
|
hermes_session_id: nextSessionId,
|
|
7646
7754
|
command_state: {
|
|
7647
7755
|
...input.manifest.command_state,
|
|
@@ -9787,9 +9895,11 @@ var ConversationRunLifecycle = class {
|
|
|
9787
9895
|
this.deps.paths,
|
|
9788
9896
|
run.profile
|
|
9789
9897
|
).catch(() => void 0) ?? run.hermes_session_id;
|
|
9790
|
-
await this.
|
|
9791
|
-
|
|
9792
|
-
|
|
9898
|
+
await this.rememberRunHermesSessionId(
|
|
9899
|
+
conversationId,
|
|
9900
|
+
runId,
|
|
9901
|
+
hermesSessionId
|
|
9902
|
+
);
|
|
9793
9903
|
const conversationHistory = await buildConversationHistory({
|
|
9794
9904
|
paths: this.deps.paths,
|
|
9795
9905
|
profileName: run.profile,
|
|
@@ -9859,9 +9969,11 @@ var ConversationRunLifecycle = class {
|
|
|
9859
9969
|
);
|
|
9860
9970
|
const responseSessionId = response.headers.get("x-hermes-session-id")?.trim();
|
|
9861
9971
|
if (responseSessionId) {
|
|
9862
|
-
await this.
|
|
9863
|
-
|
|
9864
|
-
|
|
9972
|
+
await this.rememberRunHermesSessionId(
|
|
9973
|
+
conversationId,
|
|
9974
|
+
runId,
|
|
9975
|
+
responseSessionId
|
|
9976
|
+
);
|
|
9865
9977
|
}
|
|
9866
9978
|
for await (const rawEvent of parseSseResponse(response)) {
|
|
9867
9979
|
if (controller.signal.aborted) {
|
|
@@ -10047,6 +10159,29 @@ ${attachmentLines.join("\n")}`
|
|
|
10047
10159
|
Object.assign(run, patch);
|
|
10048
10160
|
await this.deps.writeSnapshot(conversationId, snapshot);
|
|
10049
10161
|
}
|
|
10162
|
+
async rememberRunHermesSessionId(conversationId, runId, sessionId) {
|
|
10163
|
+
const normalizedSessionId = sessionId.trim();
|
|
10164
|
+
if (!normalizedSessionId) {
|
|
10165
|
+
return;
|
|
10166
|
+
}
|
|
10167
|
+
return this.deps.withConversationLock(conversationId, async () => {
|
|
10168
|
+
const snapshot = await this.deps.readSnapshot(conversationId);
|
|
10169
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
10170
|
+
if (!run) {
|
|
10171
|
+
return;
|
|
10172
|
+
}
|
|
10173
|
+
run.hermes_session_id = normalizedSessionId;
|
|
10174
|
+
await this.deps.writeSnapshot(conversationId, snapshot);
|
|
10175
|
+
const manifest = await this.deps.readActiveManifest(conversationId);
|
|
10176
|
+
const nextManifest = addHermesSessionIdToManifest(
|
|
10177
|
+
manifest,
|
|
10178
|
+
normalizedSessionId
|
|
10179
|
+
);
|
|
10180
|
+
if (nextManifest !== manifest) {
|
|
10181
|
+
await this.deps.writeManifest(nextManifest);
|
|
10182
|
+
}
|
|
10183
|
+
});
|
|
10184
|
+
}
|
|
10050
10185
|
async runHasAssistantOutput(conversationId, runId) {
|
|
10051
10186
|
const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
|
|
10052
10187
|
const run = snapshot?.runs.find((item) => item.id === runId);
|
|
@@ -10984,6 +11119,7 @@ var ConversationService = class {
|
|
|
10984
11119
|
statsOverride
|
|
10985
11120
|
),
|
|
10986
11121
|
readActiveManifest: (conversationId) => this.store.readActiveManifest(conversationId),
|
|
11122
|
+
writeManifest: (manifest) => this.store.writeManifest(manifest),
|
|
10987
11123
|
isConversationActive: (conversationId) => this.store.isConversationActive(conversationId),
|
|
10988
11124
|
writeBlob: (conversationId, input) => this.maintenance.writeBlob(conversationId, input),
|
|
10989
11125
|
syncCronDeliveries: () => this.syncCronDeliveries(),
|
|
@@ -11089,6 +11225,7 @@ var ConversationService = class {
|
|
|
11089
11225
|
title_source: isDefaultConversationTitle(title) ? "default" : "hermes",
|
|
11090
11226
|
status: "active",
|
|
11091
11227
|
hermes_session_id: `hp_${id}`,
|
|
11228
|
+
hermes_session_ids: [`hp_${id}`],
|
|
11092
11229
|
profile_uid: profile.profileUid,
|
|
11093
11230
|
profile_name_snapshot: profile.profileName,
|
|
11094
11231
|
profile: profile.profileName,
|
|
@@ -17495,6 +17632,7 @@ function connectRelayControl(options) {
|
|
|
17495
17632
|
let retryTimer = null;
|
|
17496
17633
|
let abortControllers = /* @__PURE__ */ new Map();
|
|
17497
17634
|
let fatalRelayRejection = null;
|
|
17635
|
+
let latestNetworkRoutes = null;
|
|
17498
17636
|
const connect = () => {
|
|
17499
17637
|
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
17500
17638
|
fatalRelayRejection = null;
|
|
@@ -17506,6 +17644,10 @@ function connectRelayControl(options) {
|
|
|
17506
17644
|
socket.on("open", () => {
|
|
17507
17645
|
reconnectAttempts = 0;
|
|
17508
17646
|
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
17647
|
+
const currentSocket = socket;
|
|
17648
|
+
if (currentSocket && latestNetworkRoutes) {
|
|
17649
|
+
sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
|
|
17650
|
+
}
|
|
17509
17651
|
});
|
|
17510
17652
|
socket.on("message", (raw) => {
|
|
17511
17653
|
if (!socket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
@@ -17553,6 +17695,12 @@ function connectRelayControl(options) {
|
|
|
17553
17695
|
};
|
|
17554
17696
|
connect();
|
|
17555
17697
|
return {
|
|
17698
|
+
publishNetworkRoutes(routes) {
|
|
17699
|
+
latestNetworkRoutes = routes;
|
|
17700
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
17701
|
+
sendNetworkRoutes(socket, options.linkId, routes);
|
|
17702
|
+
}
|
|
17703
|
+
},
|
|
17556
17704
|
close() {
|
|
17557
17705
|
closedByUser = true;
|
|
17558
17706
|
if (retryTimer) {
|
|
@@ -17564,6 +17712,19 @@ function connectRelayControl(options) {
|
|
|
17564
17712
|
}
|
|
17565
17713
|
};
|
|
17566
17714
|
}
|
|
17715
|
+
function sendNetworkRoutes(socket, linkId, routes) {
|
|
17716
|
+
socket.send(JSON.stringify({
|
|
17717
|
+
type: "network.routes",
|
|
17718
|
+
id: `routes_${Date.now().toString(36)}`,
|
|
17719
|
+
payload: {
|
|
17720
|
+
link_id: linkId,
|
|
17721
|
+
lan_ips: routes.lanIps,
|
|
17722
|
+
public_ipv4s: routes.publicIpv4s,
|
|
17723
|
+
public_ipv6s: routes.publicIpv6s,
|
|
17724
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17725
|
+
}
|
|
17726
|
+
}));
|
|
17727
|
+
}
|
|
17567
17728
|
function resolveFatalRelayRejection(message) {
|
|
17568
17729
|
if (!/Unexpected server response:\s*(400|401|403|426)\b/u.test(message)) {
|
|
17569
17730
|
return null;
|
|
@@ -17787,7 +17948,7 @@ async function discoverRouteCandidates(options) {
|
|
|
17787
17948
|
const environment = detectRuntimeEnvironment();
|
|
17788
17949
|
const configuredLanHost = normalizeLanHost(options.configuredLanHost);
|
|
17789
17950
|
const lanIps = configuredLanHost ? [configuredLanHost] : environment.lanAutoDiscoveryUsable ? discoverLanIps() : [];
|
|
17790
|
-
const publicIps = options.relayBootstrapToken ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
17951
|
+
const publicIps = options.relayBootstrapToken || options.observePublicRoute ? await observePublicRoute(options).catch(() => ({ publicIpv4s: [], publicIpv6s: [] })) : { publicIpv4s: [], publicIpv6s: [] };
|
|
17791
17952
|
const publicIpv4s = unique(publicIps.publicIpv4s.filter(isUsablePublicIpv4)).slice(0, MAX_PUBLIC_IPV4S);
|
|
17792
17953
|
const publicIpv6s = unique(publicIps.publicIpv6s.filter(isUsablePublicIpv6)).slice(0, MAX_PUBLIC_IPV6S);
|
|
17793
17954
|
const preferredUrls = [
|
|
@@ -17830,8 +17991,8 @@ async function observePublicRoute(options) {
|
|
|
17830
17991
|
const response = await fetcher(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/public-route/observe`, {
|
|
17831
17992
|
method: "POST",
|
|
17832
17993
|
headers: {
|
|
17833
|
-
|
|
17834
|
-
|
|
17994
|
+
"content-type": "application/json",
|
|
17995
|
+
...options.relayBootstrapToken ? { authorization: `Bearer ${options.relayBootstrapToken}` } : {}
|
|
17835
17996
|
},
|
|
17836
17997
|
body: JSON.stringify({
|
|
17837
17998
|
install_id: options.installId,
|
|
@@ -17943,25 +18104,32 @@ function unique(values) {
|
|
|
17943
18104
|
|
|
17944
18105
|
// src/link/network-report-state.ts
|
|
17945
18106
|
var DEFAULT_AUTO_DAILY_LIMIT = 20;
|
|
17946
|
-
async function markNetworkStatusReported(paths,
|
|
18107
|
+
async function markNetworkStatusReported(paths, snapshotInput, reportedAt = /* @__PURE__ */ new Date()) {
|
|
18108
|
+
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
17947
18109
|
await updateNetworkReportState(paths, (current) => ({
|
|
17948
18110
|
...current,
|
|
17949
|
-
lastReportedLanIps:
|
|
18111
|
+
lastReportedLanIps: snapshot.lanIps,
|
|
18112
|
+
lastReportedPublicIpv4s: snapshot.publicIpv4s,
|
|
18113
|
+
lastReportedPublicIpv6s: snapshot.publicIpv6s,
|
|
17950
18114
|
lastReportedAt: reportedAt.toISOString(),
|
|
17951
|
-
lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, success: true } : null
|
|
18115
|
+
lastAutoAttempt: current.lastAutoAttempt ? { ...current.lastAutoAttempt, ...snapshot, success: true } : null
|
|
17952
18116
|
}));
|
|
17953
18117
|
}
|
|
17954
|
-
async function reserveAutomaticNetworkReport(paths,
|
|
17955
|
-
const snapshot =
|
|
18118
|
+
async function reserveAutomaticNetworkReport(paths, snapshotInput, options = {}) {
|
|
18119
|
+
const snapshot = normalizeNetworkSnapshot(snapshotInput);
|
|
17956
18120
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
17957
18121
|
const dailyLimit = Math.max(0, Math.floor(options.dailyLimit ?? DEFAULT_AUTO_DAILY_LIMIT));
|
|
17958
18122
|
let reservation = { allowed: false, reason: "unchanged" };
|
|
17959
18123
|
await updateNetworkReportState(paths, (current) => {
|
|
17960
|
-
if (
|
|
17961
|
-
|
|
17962
|
-
|
|
18124
|
+
if (sameNetworkSnapshot(readReportedSnapshot(current), snapshot)) {
|
|
18125
|
+
const lastReportedAt = current.lastReportedAt ? Date.parse(current.lastReportedAt) : Number.NaN;
|
|
18126
|
+
const unchangedMinIntervalMs = Math.max(0, Math.floor(options.unchangedMinIntervalMs ?? 0));
|
|
18127
|
+
if (!options.force || Number.isFinite(lastReportedAt) && now.getTime() - lastReportedAt < unchangedMinIntervalMs) {
|
|
18128
|
+
reservation = { allowed: false, reason: "unchanged" };
|
|
18129
|
+
return current;
|
|
18130
|
+
}
|
|
17963
18131
|
}
|
|
17964
|
-
if (current.lastAutoAttempt && !current.lastAutoAttempt.success &&
|
|
18132
|
+
if (current.lastAutoAttempt && !current.lastAutoAttempt.success && sameNetworkSnapshot(readAttemptSnapshot(current.lastAutoAttempt), snapshot)) {
|
|
17965
18133
|
reservation = { allowed: false, reason: "failed_snapshot_not_retried" };
|
|
17966
18134
|
return current;
|
|
17967
18135
|
}
|
|
@@ -17977,7 +18145,7 @@ async function reserveAutomaticNetworkReport(paths, lanIps, options = {}) {
|
|
|
17977
18145
|
autoQuotaDay: quotaDay,
|
|
17978
18146
|
autoReportsToday: reportsToday + 1,
|
|
17979
18147
|
lastAutoAttempt: {
|
|
17980
|
-
|
|
18148
|
+
...snapshot,
|
|
17981
18149
|
attemptedAt: now.toISOString(),
|
|
17982
18150
|
success: false
|
|
17983
18151
|
}
|
|
@@ -18001,6 +18169,8 @@ function normalizeNetworkReportState(value) {
|
|
|
18001
18169
|
const record = value && typeof value === "object" ? value : {};
|
|
18002
18170
|
return {
|
|
18003
18171
|
lastReportedLanIps: normalizeLanIps(record.lastReportedLanIps),
|
|
18172
|
+
lastReportedPublicIpv4s: normalizeLanIps(record.lastReportedPublicIpv4s),
|
|
18173
|
+
lastReportedPublicIpv6s: normalizeLanIps(record.lastReportedPublicIpv6s),
|
|
18004
18174
|
lastReportedAt: typeof record.lastReportedAt === "string" ? record.lastReportedAt : null,
|
|
18005
18175
|
autoQuotaDay: typeof record.autoQuotaDay === "string" ? record.autoQuotaDay : null,
|
|
18006
18176
|
autoReportsToday: Number.isFinite(record.autoReportsToday) ? Math.max(0, Math.floor(record.autoReportsToday ?? 0)) : 0,
|
|
@@ -18017,10 +18187,41 @@ function normalizeAttempt(value) {
|
|
|
18017
18187
|
}
|
|
18018
18188
|
return {
|
|
18019
18189
|
lanIps: normalizeLanIps(record.lanIps),
|
|
18190
|
+
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
18191
|
+
publicIpv6s: normalizeLanIps(record.publicIpv6s),
|
|
18020
18192
|
attemptedAt: record.attemptedAt,
|
|
18021
18193
|
success: record.success === true
|
|
18022
18194
|
};
|
|
18023
18195
|
}
|
|
18196
|
+
function normalizeNetworkSnapshot(value) {
|
|
18197
|
+
if (Array.isArray(value)) {
|
|
18198
|
+
return {
|
|
18199
|
+
lanIps: normalizeLanIps(value),
|
|
18200
|
+
publicIpv4s: [],
|
|
18201
|
+
publicIpv6s: []
|
|
18202
|
+
};
|
|
18203
|
+
}
|
|
18204
|
+
const record = value && typeof value === "object" ? value : {};
|
|
18205
|
+
return {
|
|
18206
|
+
lanIps: normalizeLanIps(record.lanIps),
|
|
18207
|
+
publicIpv4s: normalizeLanIps(record.publicIpv4s),
|
|
18208
|
+
publicIpv6s: normalizeLanIps(record.publicIpv6s)
|
|
18209
|
+
};
|
|
18210
|
+
}
|
|
18211
|
+
function readReportedSnapshot(state) {
|
|
18212
|
+
return {
|
|
18213
|
+
lanIps: state.lastReportedLanIps,
|
|
18214
|
+
publicIpv4s: state.lastReportedPublicIpv4s,
|
|
18215
|
+
publicIpv6s: state.lastReportedPublicIpv6s
|
|
18216
|
+
};
|
|
18217
|
+
}
|
|
18218
|
+
function readAttemptSnapshot(attempt) {
|
|
18219
|
+
return {
|
|
18220
|
+
lanIps: attempt.lanIps,
|
|
18221
|
+
publicIpv4s: attempt.publicIpv4s,
|
|
18222
|
+
publicIpv6s: attempt.publicIpv6s
|
|
18223
|
+
};
|
|
18224
|
+
}
|
|
18024
18225
|
function normalizeLanIps(value) {
|
|
18025
18226
|
if (!Array.isArray(value)) {
|
|
18026
18227
|
return [];
|
|
@@ -18031,7 +18232,10 @@ function normalizeLanIps(value) {
|
|
|
18031
18232
|
)
|
|
18032
18233
|
];
|
|
18033
18234
|
}
|
|
18034
|
-
function
|
|
18235
|
+
function sameNetworkSnapshot(left, right) {
|
|
18236
|
+
return sameStringList(left.lanIps, right.lanIps) && sameStringList(left.publicIpv4s, right.publicIpv4s) && sameStringList(left.publicIpv6s, right.publicIpv6s);
|
|
18237
|
+
}
|
|
18238
|
+
function sameStringList(left, right) {
|
|
18035
18239
|
if (left.length !== right.length) {
|
|
18036
18240
|
return false;
|
|
18037
18241
|
}
|
|
@@ -18048,12 +18252,13 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
18048
18252
|
if (!identity?.link_id) {
|
|
18049
18253
|
return null;
|
|
18050
18254
|
}
|
|
18051
|
-
const routes = await discoverRouteCandidates({
|
|
18255
|
+
const routes = options.routes ?? await discoverRouteCandidates({
|
|
18052
18256
|
port: config.port,
|
|
18053
18257
|
relayBaseUrl: config.relayBaseUrl,
|
|
18054
18258
|
linkId: identity.link_id,
|
|
18055
18259
|
installId: identity.install_id,
|
|
18056
18260
|
publicKeyPem: identity.public_key_pem,
|
|
18261
|
+
observePublicRoute: true,
|
|
18057
18262
|
configuredLanHost: config.lanHost,
|
|
18058
18263
|
fetchImpl: options.fetchImpl
|
|
18059
18264
|
});
|
|
@@ -18093,7 +18298,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
18093
18298
|
const message = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
18094
18299
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
18095
18300
|
}
|
|
18096
|
-
await markNetworkStatusReported(paths, routes
|
|
18301
|
+
await markNetworkStatusReported(paths, routes);
|
|
18097
18302
|
return body;
|
|
18098
18303
|
}
|
|
18099
18304
|
function canonicalJson(value) {
|
|
@@ -18128,16 +18333,17 @@ function readErrorMessage3(payload) {
|
|
|
18128
18333
|
// src/daemon/lan-ip-monitor.ts
|
|
18129
18334
|
var DEFAULT_INTERVAL_MS = 5 * 6e4;
|
|
18130
18335
|
var DEFAULT_DAILY_REPORT_LIMIT = 20;
|
|
18336
|
+
var DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS = 15 * 6e4;
|
|
18131
18337
|
function startLanIpMonitor(options) {
|
|
18132
18338
|
let running = false;
|
|
18133
18339
|
let closed = false;
|
|
18134
|
-
const check = async () => {
|
|
18340
|
+
const check = async (context = {}) => {
|
|
18135
18341
|
if (running || closed) {
|
|
18136
18342
|
return;
|
|
18137
18343
|
}
|
|
18138
18344
|
running = true;
|
|
18139
18345
|
try {
|
|
18140
|
-
await checkLanIpChange(options);
|
|
18346
|
+
await checkLanIpChange(options, context);
|
|
18141
18347
|
} catch (error) {
|
|
18142
18348
|
void options.logger.warn("lan_ip_monitor_failed", {
|
|
18143
18349
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -18146,7 +18352,7 @@ function startLanIpMonitor(options) {
|
|
|
18146
18352
|
running = false;
|
|
18147
18353
|
}
|
|
18148
18354
|
};
|
|
18149
|
-
void check();
|
|
18355
|
+
void check({ forceReport: true, publishToRelay: true });
|
|
18150
18356
|
const timer = setInterval(() => {
|
|
18151
18357
|
void check();
|
|
18152
18358
|
}, options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
@@ -18158,7 +18364,7 @@ function startLanIpMonitor(options) {
|
|
|
18158
18364
|
}
|
|
18159
18365
|
};
|
|
18160
18366
|
}
|
|
18161
|
-
async function checkLanIpChange(options) {
|
|
18367
|
+
async function checkLanIpChange(options, context = {}) {
|
|
18162
18368
|
const [identity, config] = await Promise.all([
|
|
18163
18369
|
loadIdentity(options.paths),
|
|
18164
18370
|
loadConfig(options.paths)
|
|
@@ -18172,14 +18378,25 @@ async function checkLanIpChange(options) {
|
|
|
18172
18378
|
linkId: identity.link_id,
|
|
18173
18379
|
installId: identity.install_id,
|
|
18174
18380
|
publicKeyPem: identity.public_key_pem,
|
|
18381
|
+
observePublicRoute: true,
|
|
18175
18382
|
configuredLanHost: config.lanHost,
|
|
18176
18383
|
fetchImpl: options.fetchImpl
|
|
18177
18384
|
});
|
|
18178
|
-
|
|
18179
|
-
|
|
18385
|
+
if (context.publishToRelay) {
|
|
18386
|
+
options.onNetworkRoutes?.(routes);
|
|
18387
|
+
}
|
|
18388
|
+
const reservation = await reserveAutomaticNetworkReport(options.paths, routes, {
|
|
18389
|
+
dailyLimit: options.dailyReportLimit ?? DEFAULT_DAILY_REPORT_LIMIT,
|
|
18390
|
+
force: context.forceReport === true,
|
|
18391
|
+
unchangedMinIntervalMs: options.startupReportMinIntervalMs ?? DEFAULT_STARTUP_REPORT_MIN_INTERVAL_MS
|
|
18180
18392
|
});
|
|
18181
18393
|
if (!reservation.allowed) {
|
|
18182
|
-
const logFields = {
|
|
18394
|
+
const logFields = {
|
|
18395
|
+
lan_ips: routes.lanIps,
|
|
18396
|
+
public_ipv4s: routes.publicIpv4s,
|
|
18397
|
+
public_ipv6s: routes.publicIpv6s,
|
|
18398
|
+
reason: reservation.reason
|
|
18399
|
+
};
|
|
18183
18400
|
if (reservation.reason === "daily_limit_reached") {
|
|
18184
18401
|
void options.logger.warn("lan_ip_report_skipped", logFields);
|
|
18185
18402
|
} else {
|
|
@@ -18190,12 +18407,16 @@ async function checkLanIpChange(options) {
|
|
|
18190
18407
|
try {
|
|
18191
18408
|
const result = await reportLinkStatusToServer({
|
|
18192
18409
|
paths: options.paths,
|
|
18193
|
-
fetchImpl: options.fetchImpl
|
|
18410
|
+
fetchImpl: options.fetchImpl,
|
|
18411
|
+
routes
|
|
18194
18412
|
});
|
|
18195
18413
|
if (result) {
|
|
18414
|
+
options.onNetworkRoutes?.(routes);
|
|
18196
18415
|
void options.logger.info("lan_ip_change_reported", {
|
|
18197
18416
|
link_id: result.linkId,
|
|
18198
|
-
lan_ips: routes.lanIps
|
|
18417
|
+
lan_ips: routes.lanIps,
|
|
18418
|
+
public_ipv4s: routes.publicIpv4s,
|
|
18419
|
+
public_ipv6s: routes.publicIpv6s
|
|
18199
18420
|
});
|
|
18200
18421
|
}
|
|
18201
18422
|
} catch (error) {
|
|
@@ -18287,13 +18508,6 @@ async function startLinkService(options = {}) {
|
|
|
18287
18508
|
conversations,
|
|
18288
18509
|
logger
|
|
18289
18510
|
});
|
|
18290
|
-
const lanIpMonitor = startLanIpMonitor({
|
|
18291
|
-
paths,
|
|
18292
|
-
logger,
|
|
18293
|
-
intervalMs: options.lanIpMonitorIntervalMs,
|
|
18294
|
-
dailyReportLimit: options.lanIpMonitorDailyReportLimit,
|
|
18295
|
-
fetchImpl: options.lanIpMonitorFetchImpl
|
|
18296
|
-
});
|
|
18297
18511
|
let relay = null;
|
|
18298
18512
|
if (identity?.link_id) {
|
|
18299
18513
|
relay = connectRelayControl({
|
|
@@ -18310,6 +18524,16 @@ async function startLinkService(options = {}) {
|
|
|
18310
18524
|
} else {
|
|
18311
18525
|
void logger.info("relay_skipped", { reason: "link_not_paired" });
|
|
18312
18526
|
}
|
|
18527
|
+
const lanIpMonitor = startLanIpMonitor({
|
|
18528
|
+
paths,
|
|
18529
|
+
logger,
|
|
18530
|
+
intervalMs: options.lanIpMonitorIntervalMs,
|
|
18531
|
+
dailyReportLimit: options.lanIpMonitorDailyReportLimit,
|
|
18532
|
+
fetchImpl: options.lanIpMonitorFetchImpl,
|
|
18533
|
+
onNetworkRoutes: (routes) => {
|
|
18534
|
+
relay?.publishNetworkRoutes(routes);
|
|
18535
|
+
}
|
|
18536
|
+
});
|
|
18313
18537
|
if (options.writePidFile) {
|
|
18314
18538
|
await writePidFile(paths);
|
|
18315
18539
|
}
|
package/dist/cli/index.js
CHANGED
package/dist/http/app.d.ts
CHANGED
|
@@ -237,12 +237,14 @@ interface ConversationEvent {
|
|
|
237
237
|
interface DeleteConversationResult {
|
|
238
238
|
conversation_id: string;
|
|
239
239
|
hermes_deleted: boolean;
|
|
240
|
+
hermes_session_ids?: string[];
|
|
240
241
|
deleted_at: string;
|
|
241
242
|
}
|
|
242
243
|
interface BulkDeleteConversationResult {
|
|
243
244
|
conversation_id: string;
|
|
244
245
|
status: 'deleted' | 'failed';
|
|
245
246
|
hermes_deleted?: boolean;
|
|
247
|
+
hermes_session_ids?: string[];
|
|
246
248
|
deleted_at?: string;
|
|
247
249
|
error?: {
|
|
248
250
|
code: string;
|
package/dist/http/app.js
CHANGED