@hermespilot/link 0.6.7 → 0.6.9
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-FWX7ZUP4.js → chunk-DOSXOXOS.js} +1592 -322
- package/dist/cli/index.js +1 -1
- package/dist/http/app.d.ts +30 -2
- package/dist/http/app.js +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import Router from "@koa/router";
|
|
|
4
4
|
|
|
5
5
|
// src/conversations/conversation-service.ts
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
-
import { randomUUID as randomUUID10 } from "crypto";
|
|
7
|
+
import { createHash as createHash6, randomUUID as randomUUID10 } from "crypto";
|
|
8
8
|
|
|
9
9
|
// src/database/link-database.ts
|
|
10
10
|
import { mkdir } from "fs/promises";
|
|
@@ -1359,6 +1359,7 @@ function isNodeError(error, code) {
|
|
|
1359
1359
|
}
|
|
1360
1360
|
|
|
1361
1361
|
// src/storage/atomic-json.ts
|
|
1362
|
+
var jsonUpdateQueues = /* @__PURE__ */ new Map();
|
|
1362
1363
|
async function readJsonFile(filePath) {
|
|
1363
1364
|
try {
|
|
1364
1365
|
const raw = await readFile(filePath, "utf8");
|
|
@@ -1375,6 +1376,24 @@ async function writeJsonFile(filePath, value, mode = 384) {
|
|
|
1375
1376
|
`;
|
|
1376
1377
|
await atomicWriteFilePreservingMetadata(filePath, payload, { mode });
|
|
1377
1378
|
}
|
|
1379
|
+
async function updateJsonFile(filePath, update, mode = 384) {
|
|
1380
|
+
const previous = jsonUpdateQueues.get(filePath) ?? Promise.resolve();
|
|
1381
|
+
let next;
|
|
1382
|
+
const operation = previous.catch(() => void 0).then(async () => {
|
|
1383
|
+
const current = await readJsonFile(filePath);
|
|
1384
|
+
next = await update(current);
|
|
1385
|
+
await writeJsonFile(filePath, next, mode);
|
|
1386
|
+
});
|
|
1387
|
+
const queued = operation.catch(() => void 0);
|
|
1388
|
+
jsonUpdateQueues.set(filePath, queued);
|
|
1389
|
+
void queued.finally(() => {
|
|
1390
|
+
if (jsonUpdateQueues.get(filePath) === queued) {
|
|
1391
|
+
jsonUpdateQueues.delete(filePath);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
await operation;
|
|
1395
|
+
return next;
|
|
1396
|
+
}
|
|
1378
1397
|
function isNodeError2(error, code) {
|
|
1379
1398
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1380
1399
|
}
|
|
@@ -5275,6 +5294,10 @@ async function bindCronJobToHermesLink(paths, input) {
|
|
|
5275
5294
|
if (existing) {
|
|
5276
5295
|
existing.conversationId = input.conversationId;
|
|
5277
5296
|
existing.source = input.source;
|
|
5297
|
+
if (input.ownerAccountId) {
|
|
5298
|
+
existing.ownerAccountId = input.ownerAccountId;
|
|
5299
|
+
existing.ownerAppInstanceId = input.ownerAppInstanceId;
|
|
5300
|
+
}
|
|
5278
5301
|
} else {
|
|
5279
5302
|
registry.bindings.push({
|
|
5280
5303
|
...input,
|
|
@@ -5284,6 +5307,26 @@ async function bindCronJobToHermesLink(paths, input) {
|
|
|
5284
5307
|
}
|
|
5285
5308
|
await writeRegistry(paths, registry);
|
|
5286
5309
|
}
|
|
5310
|
+
async function backfillHermesLinkCronDeliveryOwner(paths, input) {
|
|
5311
|
+
const accountId = input.accountId.trim();
|
|
5312
|
+
if (!accountId) {
|
|
5313
|
+
return 0;
|
|
5314
|
+
}
|
|
5315
|
+
const registry = await readRegistry(paths);
|
|
5316
|
+
let changed = 0;
|
|
5317
|
+
for (const binding of registry.bindings) {
|
|
5318
|
+
if (binding.ownerAccountId) {
|
|
5319
|
+
continue;
|
|
5320
|
+
}
|
|
5321
|
+
binding.ownerAccountId = accountId;
|
|
5322
|
+
binding.ownerAppInstanceId = input.appInstanceId;
|
|
5323
|
+
changed += 1;
|
|
5324
|
+
}
|
|
5325
|
+
if (changed > 0) {
|
|
5326
|
+
await writeRegistry(paths, registry);
|
|
5327
|
+
}
|
|
5328
|
+
return changed;
|
|
5329
|
+
}
|
|
5287
5330
|
async function bindNewCronJobsToHermesLink(paths, input) {
|
|
5288
5331
|
for (const job of input.jobs) {
|
|
5289
5332
|
const jobId = readString3(job, "id") ?? readString3(job, "job_id");
|
|
@@ -5298,7 +5341,9 @@ async function bindNewCronJobsToHermesLink(paths, input) {
|
|
|
5298
5341
|
profileName: input.profileName,
|
|
5299
5342
|
jobId,
|
|
5300
5343
|
conversationId: input.conversationId,
|
|
5301
|
-
source: "natural_language"
|
|
5344
|
+
source: "natural_language",
|
|
5345
|
+
ownerAccountId: input.ownerAccountId,
|
|
5346
|
+
ownerAppInstanceId: input.ownerAppInstanceId
|
|
5302
5347
|
});
|
|
5303
5348
|
}
|
|
5304
5349
|
}
|
|
@@ -5323,6 +5368,28 @@ async function decorateHermesLinkCronJob(paths, profileName, job) {
|
|
|
5323
5368
|
);
|
|
5324
5369
|
return binding ? { ...job, deliver: HERMES_LINK_CRON_DELIVER } : job;
|
|
5325
5370
|
}
|
|
5371
|
+
async function listHermesLinkCronOutputWatchDirs(paths) {
|
|
5372
|
+
const registry = await readRegistry(paths);
|
|
5373
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
5374
|
+
for (const binding of registry.bindings) {
|
|
5375
|
+
dirs.add(
|
|
5376
|
+
path4.join(resolveHermesProfileDir(binding.profileName), "cron", "output")
|
|
5377
|
+
);
|
|
5378
|
+
}
|
|
5379
|
+
const existing = [];
|
|
5380
|
+
for (const dir of dirs) {
|
|
5381
|
+
const dirStat = await stat2(dir).catch((error) => {
|
|
5382
|
+
if (isNodeError4(error, "ENOENT")) {
|
|
5383
|
+
return null;
|
|
5384
|
+
}
|
|
5385
|
+
throw error;
|
|
5386
|
+
});
|
|
5387
|
+
if (dirStat?.isDirectory()) {
|
|
5388
|
+
existing.push(dir);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
return existing;
|
|
5392
|
+
}
|
|
5326
5393
|
async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
5327
5394
|
const registry = await readRegistry(paths);
|
|
5328
5395
|
let touched = false;
|
|
@@ -5339,15 +5406,27 @@ async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
|
5339
5406
|
}
|
|
5340
5407
|
try {
|
|
5341
5408
|
const content = await readCronOutput(output.path);
|
|
5342
|
-
await runtime.appendCronDelivery({
|
|
5409
|
+
const handled = await runtime.appendCronDelivery({
|
|
5343
5410
|
conversationId: binding.conversationId,
|
|
5344
5411
|
profileName: binding.profileName,
|
|
5345
5412
|
jobId: binding.jobId,
|
|
5413
|
+
source: binding.source,
|
|
5346
5414
|
jobName: await readCronJobNameFromOutput(content),
|
|
5347
5415
|
outputPath: output.path,
|
|
5348
5416
|
content,
|
|
5349
|
-
|
|
5417
|
+
failed: isFailedCronOutput(content),
|
|
5418
|
+
runAt: output.mtime,
|
|
5419
|
+
accountId: binding.ownerAccountId,
|
|
5420
|
+
appInstanceId: binding.ownerAppInstanceId
|
|
5350
5421
|
});
|
|
5422
|
+
if (!handled) {
|
|
5423
|
+
void logger.warn("cron_link_delivery_pending", {
|
|
5424
|
+
profile: binding.profileName,
|
|
5425
|
+
job_id: binding.jobId,
|
|
5426
|
+
output_path: output.path
|
|
5427
|
+
});
|
|
5428
|
+
continue;
|
|
5429
|
+
}
|
|
5351
5430
|
delivered.add(output.path);
|
|
5352
5431
|
touched = true;
|
|
5353
5432
|
} catch (error) {
|
|
@@ -5423,6 +5502,9 @@ async function readCronJobNameFromOutput(content) {
|
|
|
5423
5502
|
const match = content.match(/^#\s*Cron Job:\s*(.+)$/mu);
|
|
5424
5503
|
return match?.[1]?.trim() || void 0;
|
|
5425
5504
|
}
|
|
5505
|
+
function isFailedCronOutput(content) {
|
|
5506
|
+
return /^#\s*Cron Job:\s*.+\(FAILED\)\s*$/imu.test(content) || /^\*\*Status:\*\*\s*(?:script\s+)?failed\s*$/imu.test(content) || /^##\s+Error\s*$/imu.test(content);
|
|
5507
|
+
}
|
|
5426
5508
|
async function readRegistry(paths) {
|
|
5427
5509
|
const existing = await readJsonFile(registryPath(paths));
|
|
5428
5510
|
if (existing?.version === REGISTRY_VERSION && Array.isArray(existing.bindings)) {
|
|
@@ -5449,7 +5531,7 @@ function normalizeDeliverValue(value) {
|
|
|
5449
5531
|
}
|
|
5450
5532
|
function isValidBinding(value) {
|
|
5451
5533
|
const binding = value;
|
|
5452
|
-
return typeof binding?.profileName === "string" && typeof binding.jobId === "string" && typeof binding.conversationId === "string" && (binding.source === "app" || binding.source === "natural_language");
|
|
5534
|
+
return typeof binding?.profileName === "string" && typeof binding.jobId === "string" && (binding.conversationId === void 0 || typeof binding.conversationId === "string") && (binding.source === "app" || binding.source === "natural_language");
|
|
5453
5535
|
}
|
|
5454
5536
|
function readString3(record, ...keys) {
|
|
5455
5537
|
for (const key of keys) {
|
|
@@ -5467,30 +5549,15 @@ function isConversationMissingError(error) {
|
|
|
5467
5549
|
return isLinkHttpError(error) && error.status === 404 && error.code === "conversation_not_found";
|
|
5468
5550
|
}
|
|
5469
5551
|
|
|
5470
|
-
// src/hermes/gateway.ts
|
|
5471
|
-
import { execFile as execFile2, spawn } from "child_process";
|
|
5472
|
-
import { constants as fsConstants } from "fs";
|
|
5473
|
-
import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
|
|
5474
|
-
import path7 from "path";
|
|
5475
|
-
import { setTimeout as delay2 } from "timers/promises";
|
|
5476
|
-
import { promisify as promisify2 } from "util";
|
|
5477
|
-
|
|
5478
|
-
// src/runtime/logger.ts
|
|
5479
|
-
import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, truncate } from "fs/promises";
|
|
5480
|
-
import os3 from "os";
|
|
5481
|
-
import path6 from "path";
|
|
5482
|
-
|
|
5483
|
-
// src/runtime/paths.ts
|
|
5484
|
-
import os2 from "os";
|
|
5485
|
-
import path5 from "path";
|
|
5486
|
-
|
|
5487
5552
|
// src/constants.ts
|
|
5488
|
-
var LINK_VERSION = "0.6.
|
|
5553
|
+
var LINK_VERSION = "0.6.9";
|
|
5489
5554
|
var LINK_COMMAND = "hermeslink";
|
|
5490
5555
|
var LINK_DEFAULT_PORT = 52379;
|
|
5491
5556
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
5492
5557
|
|
|
5493
5558
|
// src/runtime/paths.ts
|
|
5559
|
+
import os2 from "os";
|
|
5560
|
+
import path5 from "path";
|
|
5494
5561
|
function resolveRuntimeHome() {
|
|
5495
5562
|
return process.env.HERMESLINK_HOME?.trim() ? path5.resolve(process.env.HERMESLINK_HOME) : path5.join(os2.homedir(), LINK_RUNTIME_DIR_NAME);
|
|
5496
5563
|
}
|
|
@@ -5511,7 +5578,255 @@ function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
|
|
|
5511
5578
|
};
|
|
5512
5579
|
}
|
|
5513
5580
|
|
|
5581
|
+
// src/config/config.ts
|
|
5582
|
+
var defaultLinkConfig = {
|
|
5583
|
+
port: LINK_DEFAULT_PORT,
|
|
5584
|
+
lanHost: null,
|
|
5585
|
+
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
5586
|
+
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
5587
|
+
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
5588
|
+
appConnectTokenAudience: "hermes-link",
|
|
5589
|
+
language: "auto",
|
|
5590
|
+
logLevel: "warn"
|
|
5591
|
+
};
|
|
5592
|
+
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
5593
|
+
const existing = await readJsonFile(paths.configFile);
|
|
5594
|
+
const language = normalizeConfiguredLanguage(existing?.language);
|
|
5595
|
+
const lanHost = normalizeLanHost(existing?.lanHost);
|
|
5596
|
+
const logLevel = normalizeLogLevel(
|
|
5597
|
+
existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL
|
|
5598
|
+
);
|
|
5599
|
+
return {
|
|
5600
|
+
...defaultLinkConfig,
|
|
5601
|
+
...existing ?? {},
|
|
5602
|
+
language,
|
|
5603
|
+
lanHost,
|
|
5604
|
+
logLevel
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
async function saveConfig(patch, paths = resolveRuntimePaths()) {
|
|
5608
|
+
const current = await loadConfig(paths);
|
|
5609
|
+
const next = {
|
|
5610
|
+
...current,
|
|
5611
|
+
...patch,
|
|
5612
|
+
logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
|
|
5613
|
+
};
|
|
5614
|
+
await writeJsonFile(paths.configFile, next);
|
|
5615
|
+
return next;
|
|
5616
|
+
}
|
|
5617
|
+
function normalizeConfiguredLanguage(language) {
|
|
5618
|
+
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
5619
|
+
return language;
|
|
5620
|
+
}
|
|
5621
|
+
return defaultLinkConfig.language;
|
|
5622
|
+
}
|
|
5623
|
+
function normalizeLogLevel(level) {
|
|
5624
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
5625
|
+
return level;
|
|
5626
|
+
}
|
|
5627
|
+
return defaultLinkConfig.logLevel;
|
|
5628
|
+
}
|
|
5629
|
+
function parseLogLevel(value) {
|
|
5630
|
+
if (value === "debug" || value === "info" || value === "warn" || value === "error") {
|
|
5631
|
+
return value;
|
|
5632
|
+
}
|
|
5633
|
+
return null;
|
|
5634
|
+
}
|
|
5635
|
+
function normalizeLanHost(value) {
|
|
5636
|
+
if (value === null || value === void 0) {
|
|
5637
|
+
return null;
|
|
5638
|
+
}
|
|
5639
|
+
if (typeof value !== "string") {
|
|
5640
|
+
return null;
|
|
5641
|
+
}
|
|
5642
|
+
const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
|
|
5643
|
+
if (!host) {
|
|
5644
|
+
return null;
|
|
5645
|
+
}
|
|
5646
|
+
if (!isUsableLanIpv4(host)) {
|
|
5647
|
+
return null;
|
|
5648
|
+
}
|
|
5649
|
+
return host;
|
|
5650
|
+
}
|
|
5651
|
+
function isUsableLanIpv4(value) {
|
|
5652
|
+
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
|
|
5653
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
5654
|
+
return false;
|
|
5655
|
+
}
|
|
5656
|
+
const [first, second, , fourth] = parts;
|
|
5657
|
+
const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
5658
|
+
return privateRange && fourth !== 0 && fourth !== 255;
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5661
|
+
// src/identity/identity.ts
|
|
5662
|
+
import { generateKeyPairSync, randomUUID as randomUUID3, sign } from "crypto";
|
|
5663
|
+
import { mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
|
|
5664
|
+
import { z } from "zod";
|
|
5665
|
+
var linkIdentitySchema = z.object({
|
|
5666
|
+
install_id: z.string().min(1),
|
|
5667
|
+
link_id: z.string().min(1).nullable().optional(),
|
|
5668
|
+
public_key_pem: z.string().min(1),
|
|
5669
|
+
private_key_pem: z.string().min(1),
|
|
5670
|
+
created_at: z.string().min(1),
|
|
5671
|
+
updated_at: z.string().min(1)
|
|
5672
|
+
});
|
|
5673
|
+
async function loadIdentity(paths = resolveRuntimePaths()) {
|
|
5674
|
+
const value = await readJsonFile(paths.identityFile);
|
|
5675
|
+
if (value === null) {
|
|
5676
|
+
return null;
|
|
5677
|
+
}
|
|
5678
|
+
return linkIdentitySchema.parse(value);
|
|
5679
|
+
}
|
|
5680
|
+
async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
5681
|
+
const existing = await loadIdentity(paths);
|
|
5682
|
+
if (existing) {
|
|
5683
|
+
return existing;
|
|
5684
|
+
}
|
|
5685
|
+
await mkdir4(paths.homeDir, { recursive: true, mode: 448 });
|
|
5686
|
+
await chmod2(paths.homeDir, 448).catch(() => void 0);
|
|
5687
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
5688
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5689
|
+
const identity = {
|
|
5690
|
+
install_id: `install_${randomUUID3().replaceAll("-", "")}`,
|
|
5691
|
+
link_id: null,
|
|
5692
|
+
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
5693
|
+
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
5694
|
+
created_at: now,
|
|
5695
|
+
updated_at: now
|
|
5696
|
+
};
|
|
5697
|
+
await writeJsonFile(paths.identityFile, identity);
|
|
5698
|
+
return identity;
|
|
5699
|
+
}
|
|
5700
|
+
async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
|
|
5701
|
+
const identity = await ensureIdentity(paths);
|
|
5702
|
+
const next = {
|
|
5703
|
+
...identity,
|
|
5704
|
+
link_id: linkId,
|
|
5705
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5706
|
+
};
|
|
5707
|
+
await writeJsonFile(paths.identityFile, next);
|
|
5708
|
+
return next;
|
|
5709
|
+
}
|
|
5710
|
+
function signRelayNonce(identity, nonce) {
|
|
5711
|
+
return signIdentityPayload(identity, nonce);
|
|
5712
|
+
}
|
|
5713
|
+
function signIdentityPayload(identity, payload) {
|
|
5714
|
+
const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
|
|
5715
|
+
return signature.toString("base64url");
|
|
5716
|
+
}
|
|
5717
|
+
function getIdentityStatus(identity) {
|
|
5718
|
+
return {
|
|
5719
|
+
installId: identity.install_id,
|
|
5720
|
+
linkId: identity.link_id ?? null,
|
|
5721
|
+
hasPrivateKey: identity.private_key_pem.trim().length > 0,
|
|
5722
|
+
publicKeyPem: identity.public_key_pem
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
// src/link/notification-events.ts
|
|
5727
|
+
async function reportNotificationEventToServer(options) {
|
|
5728
|
+
const [identity, config] = await Promise.all([
|
|
5729
|
+
loadIdentity(options.paths),
|
|
5730
|
+
loadConfig(options.paths)
|
|
5731
|
+
]);
|
|
5732
|
+
if (!identity?.link_id) {
|
|
5733
|
+
return;
|
|
5734
|
+
}
|
|
5735
|
+
const occurredAt = normalizeIso(options.event.occurredAt) ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
5736
|
+
const reportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5737
|
+
const payload = {
|
|
5738
|
+
type: "hermes_link_notification_event",
|
|
5739
|
+
link_id: identity.link_id,
|
|
5740
|
+
install_id: identity.install_id,
|
|
5741
|
+
source_event_id: options.event.sourceEventId,
|
|
5742
|
+
event_kind: options.event.eventKind,
|
|
5743
|
+
conversation_id: options.event.conversationId,
|
|
5744
|
+
occurred_at: occurredAt,
|
|
5745
|
+
reported_at: reportedAt
|
|
5746
|
+
};
|
|
5747
|
+
addOptional(payload, "account_id", options.event.accountId, 128);
|
|
5748
|
+
addOptional(payload, "message_id", options.event.messageId, 128);
|
|
5749
|
+
addOptional(payload, "run_id", options.event.runId, 128);
|
|
5750
|
+
addOptional(payload, "body_preview", options.event.bodyPreview, 512);
|
|
5751
|
+
addOptional(payload, "conversation_title", options.event.conversationTitle, 128);
|
|
5752
|
+
const signature = signIdentityPayload(identity, canonicalJson(payload));
|
|
5753
|
+
const fetcher = options.fetchImpl ?? fetch;
|
|
5754
|
+
const response = await fetcher(
|
|
5755
|
+
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/notification-events`,
|
|
5756
|
+
{
|
|
5757
|
+
method: "POST",
|
|
5758
|
+
headers: {
|
|
5759
|
+
accept: "application/json",
|
|
5760
|
+
"content-type": "application/json"
|
|
5761
|
+
},
|
|
5762
|
+
body: JSON.stringify({
|
|
5763
|
+
...payload,
|
|
5764
|
+
public_key_pem: identity.public_key_pem,
|
|
5765
|
+
signature
|
|
5766
|
+
})
|
|
5767
|
+
}
|
|
5768
|
+
);
|
|
5769
|
+
if (!response.ok) {
|
|
5770
|
+
const body = await response.json().catch(() => null);
|
|
5771
|
+
const message = readErrorMessage(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
5772
|
+
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
function addOptional(payload, key, value, maxLength) {
|
|
5776
|
+
const normalized = value?.trim();
|
|
5777
|
+
if (normalized) {
|
|
5778
|
+
payload[key] = normalized.slice(0, maxLength);
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
function normalizeIso(value) {
|
|
5782
|
+
const normalized = value?.trim();
|
|
5783
|
+
if (!normalized) {
|
|
5784
|
+
return null;
|
|
5785
|
+
}
|
|
5786
|
+
const date = new Date(normalized);
|
|
5787
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
5788
|
+
}
|
|
5789
|
+
function canonicalJson(value) {
|
|
5790
|
+
return JSON.stringify(sortJsonValue(value));
|
|
5791
|
+
}
|
|
5792
|
+
function sortJsonValue(value) {
|
|
5793
|
+
if (Array.isArray(value)) {
|
|
5794
|
+
return value.map(sortJsonValue);
|
|
5795
|
+
}
|
|
5796
|
+
if (value && typeof value === "object") {
|
|
5797
|
+
const record = value;
|
|
5798
|
+
const sorted = {};
|
|
5799
|
+
for (const key of Object.keys(record).sort()) {
|
|
5800
|
+
sorted[key] = sortJsonValue(record[key]);
|
|
5801
|
+
}
|
|
5802
|
+
return sorted;
|
|
5803
|
+
}
|
|
5804
|
+
return value;
|
|
5805
|
+
}
|
|
5806
|
+
function readErrorMessage(payload) {
|
|
5807
|
+
if (typeof payload !== "object" || payload === null) {
|
|
5808
|
+
return null;
|
|
5809
|
+
}
|
|
5810
|
+
const error = payload.error;
|
|
5811
|
+
if (typeof error !== "object" || error === null) {
|
|
5812
|
+
return null;
|
|
5813
|
+
}
|
|
5814
|
+
const message = error.message;
|
|
5815
|
+
return typeof message === "string" ? message : null;
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5818
|
+
// src/hermes/gateway.ts
|
|
5819
|
+
import { execFile as execFile2, spawn } from "child_process";
|
|
5820
|
+
import { constants as fsConstants } from "fs";
|
|
5821
|
+
import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
|
|
5822
|
+
import path7 from "path";
|
|
5823
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
5824
|
+
import { promisify as promisify2 } from "util";
|
|
5825
|
+
|
|
5514
5826
|
// src/runtime/logger.ts
|
|
5827
|
+
import { appendFile, mkdir as mkdir5, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, truncate } from "fs/promises";
|
|
5828
|
+
import os3 from "os";
|
|
5829
|
+
import path6 from "path";
|
|
5515
5830
|
var DEFAULT_LOG_FILE = "hermeslink.log";
|
|
5516
5831
|
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
5517
5832
|
var DEFAULT_MAX_FILES = 5;
|
|
@@ -5572,7 +5887,7 @@ var FileLogger = class {
|
|
|
5572
5887
|
return this.queue;
|
|
5573
5888
|
}
|
|
5574
5889
|
async appendEntry(entry) {
|
|
5575
|
-
await
|
|
5890
|
+
await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
5576
5891
|
const line = `${JSON.stringify(entry)}
|
|
5577
5892
|
`;
|
|
5578
5893
|
await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
@@ -5608,7 +5923,7 @@ function createRotatingTextLogWriter(options) {
|
|
|
5608
5923
|
return queue;
|
|
5609
5924
|
}
|
|
5610
5925
|
const next = queue.then(async () => {
|
|
5611
|
-
await
|
|
5926
|
+
await mkdir5(paths.logsDir, { recursive: true, mode: 448 });
|
|
5612
5927
|
await rotateLogFileIfNeeded(filePath, buffer.length, maxFileBytes, maxFiles);
|
|
5613
5928
|
await appendFile(filePath, buffer, { mode: 384 });
|
|
5614
5929
|
}).catch(() => void 0);
|
|
@@ -7546,8 +7861,8 @@ function firstRecord(...values) {
|
|
|
7546
7861
|
}
|
|
7547
7862
|
|
|
7548
7863
|
// src/conversations/blob-store.ts
|
|
7549
|
-
import { randomUUID as
|
|
7550
|
-
import { mkdir as
|
|
7864
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7865
|
+
import { mkdir as mkdir6, readFile as readFile6, readdir as readdir4, rm as rm3, stat as stat5, writeFile } from "fs/promises";
|
|
7551
7866
|
import path9 from "path";
|
|
7552
7867
|
|
|
7553
7868
|
// src/conversations/media.ts
|
|
@@ -8000,9 +8315,9 @@ async function writeConversationBlob(paths, conversationId, input, options) {
|
|
|
8000
8315
|
if (input.bytes.byteLength > options.maxBytes) {
|
|
8001
8316
|
throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
|
|
8002
8317
|
}
|
|
8003
|
-
const id = `blob_${
|
|
8318
|
+
const id = `blob_${randomUUID4().replaceAll("-", "")}`;
|
|
8004
8319
|
const filePath = blobPath(paths, id);
|
|
8005
|
-
await
|
|
8320
|
+
await mkdir6(path9.dirname(filePath), { recursive: true, mode: 448 });
|
|
8006
8321
|
await writeFile(filePath, input.bytes, { mode: 384 });
|
|
8007
8322
|
const blob = {
|
|
8008
8323
|
id,
|
|
@@ -8082,7 +8397,7 @@ async function materializeConversationBlob(paths, conversationId, blobId, manife
|
|
|
8082
8397
|
targetDir,
|
|
8083
8398
|
materializedAttachmentFilename(blobId, manifest.filename ?? blobId)
|
|
8084
8399
|
);
|
|
8085
|
-
await
|
|
8400
|
+
await mkdir6(targetDir, { recursive: true, mode: 448 });
|
|
8086
8401
|
await writeFile(targetPath, await readFile6(blobPath(paths, blobId)), {
|
|
8087
8402
|
mode: 384
|
|
8088
8403
|
});
|
|
@@ -8112,7 +8427,7 @@ async function pruneConversationBlobReference(paths, conversationId, blobId) {
|
|
|
8112
8427
|
}
|
|
8113
8428
|
async function listConversationBlobIds(paths, conversationId) {
|
|
8114
8429
|
assertValidConversationId(conversationId);
|
|
8115
|
-
await
|
|
8430
|
+
await mkdir6(paths.blobsDir, { recursive: true, mode: 448 });
|
|
8116
8431
|
const entries = await readdir4(paths.blobsDir, {
|
|
8117
8432
|
withFileTypes: true
|
|
8118
8433
|
}).catch((error) => {
|
|
@@ -8340,6 +8655,9 @@ function hasRunningRuns(snapshot) {
|
|
|
8340
8655
|
function hasQueuedRuns(snapshot) {
|
|
8341
8656
|
return snapshot.runs.some((run) => run.status === "queued");
|
|
8342
8657
|
}
|
|
8658
|
+
function queuedRunCount(snapshot) {
|
|
8659
|
+
return snapshot.runs.filter((run) => run.status === "queued").length;
|
|
8660
|
+
}
|
|
8343
8661
|
function buildConversationEventStreamState(snapshot) {
|
|
8344
8662
|
const pendingApprovalRunIds = /* @__PURE__ */ new Set();
|
|
8345
8663
|
let hasPendingApproval = false;
|
|
@@ -8437,7 +8755,7 @@ function isRealtimeRunStatus(status) {
|
|
|
8437
8755
|
}
|
|
8438
8756
|
|
|
8439
8757
|
// src/conversations/slash-commands.ts
|
|
8440
|
-
import { randomUUID as
|
|
8758
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
8441
8759
|
var MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,127}$/u;
|
|
8442
8760
|
function isValidModelId(value) {
|
|
8443
8761
|
return MODEL_ID_PATTERN.test(value);
|
|
@@ -8534,7 +8852,7 @@ function parseSlashCommandInput(content) {
|
|
|
8534
8852
|
}
|
|
8535
8853
|
function createSlashCommandUserMessage(input) {
|
|
8536
8854
|
return {
|
|
8537
|
-
id: `msg_${
|
|
8855
|
+
id: `msg_${randomUUID5().replaceAll("-", "")}`,
|
|
8538
8856
|
schema_version: 1,
|
|
8539
8857
|
conversation_id: input.conversationId,
|
|
8540
8858
|
role: "user",
|
|
@@ -8568,7 +8886,7 @@ function slashHelpMessage() {
|
|
|
8568
8886
|
].join("\n");
|
|
8569
8887
|
}
|
|
8570
8888
|
function freshHermesSessionId(conversationId) {
|
|
8571
|
-
return `hp_${conversationId}_${
|
|
8889
|
+
return `hp_${conversationId}_${randomUUID5().replaceAll("-", "").slice(0, 12)}`;
|
|
8572
8890
|
}
|
|
8573
8891
|
function nextVerboseMode(current) {
|
|
8574
8892
|
const modes = [
|
|
@@ -8909,11 +9227,11 @@ function formatContextUsageLines(runtime) {
|
|
|
8909
9227
|
}
|
|
8910
9228
|
|
|
8911
9229
|
// src/conversations/delivery-staging.ts
|
|
8912
|
-
import { mkdir as
|
|
9230
|
+
import { mkdir as mkdir7, rm as rm4 } from "fs/promises";
|
|
8913
9231
|
import path10 from "path";
|
|
8914
9232
|
async function prepareDeliveryStagingRunDir(paths, conversationId, runId) {
|
|
8915
9233
|
const directory = deliveryStagingRunDir(paths, conversationId, runId);
|
|
8916
|
-
await
|
|
9234
|
+
await mkdir7(directory, { recursive: true, mode: 448 });
|
|
8917
9235
|
return directory;
|
|
8918
9236
|
}
|
|
8919
9237
|
async function removeConversationDeliveryStaging(paths, conversationId) {
|
|
@@ -8938,8 +9256,8 @@ function safePathSegment(value, fallback) {
|
|
|
8938
9256
|
}
|
|
8939
9257
|
|
|
8940
9258
|
// src/conversations/conversation-archive-plans.ts
|
|
8941
|
-
import { randomUUID as
|
|
8942
|
-
import { mkdir as
|
|
9259
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9260
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
8943
9261
|
import path11 from "path";
|
|
8944
9262
|
var PLAN_ID_PATTERN = /^archive_[a-f0-9]{32}$/u;
|
|
8945
9263
|
var ConversationArchivePlanStore = class {
|
|
@@ -8950,7 +9268,7 @@ var ConversationArchivePlanStore = class {
|
|
|
8950
9268
|
async create(conversationIds) {
|
|
8951
9269
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8952
9270
|
const plan = {
|
|
8953
|
-
id: `archive_${
|
|
9271
|
+
id: `archive_${randomUUID6().replaceAll("-", "")}`,
|
|
8954
9272
|
status: "prepared",
|
|
8955
9273
|
created_at: now,
|
|
8956
9274
|
updated_at: now,
|
|
@@ -8979,7 +9297,7 @@ var ConversationArchivePlanStore = class {
|
|
|
8979
9297
|
}
|
|
8980
9298
|
async write(plan) {
|
|
8981
9299
|
const normalizedPlanId = normalizePlanId(plan.id);
|
|
8982
|
-
await
|
|
9300
|
+
await mkdir8(this.plansDir(), { recursive: true, mode: 448 });
|
|
8983
9301
|
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
8984
9302
|
}
|
|
8985
9303
|
plansDir() {
|
|
@@ -9002,8 +9320,8 @@ function normalizePlanId(planId) {
|
|
|
9002
9320
|
}
|
|
9003
9321
|
|
|
9004
9322
|
// src/conversations/conversation-clear-plans.ts
|
|
9005
|
-
import { randomUUID as
|
|
9006
|
-
import { mkdir as
|
|
9323
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9324
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
9007
9325
|
import path12 from "path";
|
|
9008
9326
|
var PLAN_ID_PATTERN2 = /^clear_[a-f0-9]{32}$/u;
|
|
9009
9327
|
var ConversationClearPlanStore = class {
|
|
@@ -9014,7 +9332,7 @@ var ConversationClearPlanStore = class {
|
|
|
9014
9332
|
async create(conversationIds, targetStatus = "active") {
|
|
9015
9333
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9016
9334
|
const plan = {
|
|
9017
|
-
id: `clear_${
|
|
9335
|
+
id: `clear_${randomUUID7().replaceAll("-", "")}`,
|
|
9018
9336
|
status: "prepared",
|
|
9019
9337
|
target_status: targetStatus,
|
|
9020
9338
|
created_at: now,
|
|
@@ -9044,7 +9362,7 @@ var ConversationClearPlanStore = class {
|
|
|
9044
9362
|
}
|
|
9045
9363
|
async write(plan) {
|
|
9046
9364
|
const normalizedPlanId = normalizePlanId2(plan.id);
|
|
9047
|
-
await
|
|
9365
|
+
await mkdir9(this.plansDir(), { recursive: true, mode: 448 });
|
|
9048
9366
|
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
9049
9367
|
}
|
|
9050
9368
|
plansDir() {
|
|
@@ -9123,6 +9441,7 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
9123
9441
|
clearPlans;
|
|
9124
9442
|
archivePlans;
|
|
9125
9443
|
async prepareClearAllConversationPlan(targetStatus = "active") {
|
|
9444
|
+
assertArchivedClearPlanTarget(targetStatus);
|
|
9126
9445
|
const targets = [];
|
|
9127
9446
|
for (const conversationId of await this.deps.store.listConversationIds()) {
|
|
9128
9447
|
const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
|
|
@@ -9145,6 +9464,7 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
9145
9464
|
}
|
|
9146
9465
|
async executeClearAllConversationPlan(planId) {
|
|
9147
9466
|
let plan = await this.clearPlans.read(planId);
|
|
9467
|
+
assertArchivedClearPlanTarget(plan.target_status ?? "active");
|
|
9148
9468
|
if (plan.status === "completed") {
|
|
9149
9469
|
return plan;
|
|
9150
9470
|
}
|
|
@@ -9207,6 +9527,7 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
9207
9527
|
}
|
|
9208
9528
|
async startClearAllConversationPlan(planId) {
|
|
9209
9529
|
const plan = await this.clearPlans.read(planId);
|
|
9530
|
+
assertArchivedClearPlanTarget(plan.target_status ?? "active");
|
|
9210
9531
|
if (plan.status === "completed" || plan.status === "executing") {
|
|
9211
9532
|
return plan;
|
|
9212
9533
|
}
|
|
@@ -9688,6 +10009,16 @@ var ConversationMaintenanceCoordinator = class {
|
|
|
9688
10009
|
return plan;
|
|
9689
10010
|
}
|
|
9690
10011
|
};
|
|
10012
|
+
function assertArchivedClearPlanTarget(targetStatus) {
|
|
10013
|
+
if (targetStatus === "archived") {
|
|
10014
|
+
return;
|
|
10015
|
+
}
|
|
10016
|
+
throw new LinkHttpError(
|
|
10017
|
+
409,
|
|
10018
|
+
"active_conversation_clear_plan_disabled",
|
|
10019
|
+
"Bulk deletion of active conversations is disabled. Archive active conversations first, or delete explicitly selected conversations."
|
|
10020
|
+
);
|
|
10021
|
+
}
|
|
9691
10022
|
function isVoiceAttachmentInput(attachment) {
|
|
9692
10023
|
return attachment.kind === "voice" || attachment.type === "voice" || attachment.is_voice_note === true || attachment.isVoiceNote === true;
|
|
9693
10024
|
}
|
|
@@ -9705,86 +10036,6 @@ function readAttachmentWaveform(attachment) {
|
|
|
9705
10036
|
).filter((item) => item !== void 0).slice(0, 96);
|
|
9706
10037
|
}
|
|
9707
10038
|
|
|
9708
|
-
// src/config/config.ts
|
|
9709
|
-
var defaultLinkConfig = {
|
|
9710
|
-
port: LINK_DEFAULT_PORT,
|
|
9711
|
-
lanHost: null,
|
|
9712
|
-
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
9713
|
-
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
9714
|
-
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
9715
|
-
appConnectTokenAudience: "hermes-link",
|
|
9716
|
-
language: "auto",
|
|
9717
|
-
logLevel: "warn"
|
|
9718
|
-
};
|
|
9719
|
-
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
9720
|
-
const existing = await readJsonFile(paths.configFile);
|
|
9721
|
-
const language = normalizeConfiguredLanguage(existing?.language);
|
|
9722
|
-
const lanHost = normalizeLanHost(existing?.lanHost);
|
|
9723
|
-
const logLevel = normalizeLogLevel(
|
|
9724
|
-
existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL
|
|
9725
|
-
);
|
|
9726
|
-
return {
|
|
9727
|
-
...defaultLinkConfig,
|
|
9728
|
-
...existing ?? {},
|
|
9729
|
-
language,
|
|
9730
|
-
lanHost,
|
|
9731
|
-
logLevel
|
|
9732
|
-
};
|
|
9733
|
-
}
|
|
9734
|
-
async function saveConfig(patch, paths = resolveRuntimePaths()) {
|
|
9735
|
-
const current = await loadConfig(paths);
|
|
9736
|
-
const next = {
|
|
9737
|
-
...current,
|
|
9738
|
-
...patch,
|
|
9739
|
-
logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
|
|
9740
|
-
};
|
|
9741
|
-
await writeJsonFile(paths.configFile, next);
|
|
9742
|
-
return next;
|
|
9743
|
-
}
|
|
9744
|
-
function normalizeConfiguredLanguage(language) {
|
|
9745
|
-
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
9746
|
-
return language;
|
|
9747
|
-
}
|
|
9748
|
-
return defaultLinkConfig.language;
|
|
9749
|
-
}
|
|
9750
|
-
function normalizeLogLevel(level) {
|
|
9751
|
-
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
9752
|
-
return level;
|
|
9753
|
-
}
|
|
9754
|
-
return defaultLinkConfig.logLevel;
|
|
9755
|
-
}
|
|
9756
|
-
function parseLogLevel(value) {
|
|
9757
|
-
if (value === "debug" || value === "info" || value === "warn" || value === "error") {
|
|
9758
|
-
return value;
|
|
9759
|
-
}
|
|
9760
|
-
return null;
|
|
9761
|
-
}
|
|
9762
|
-
function normalizeLanHost(value) {
|
|
9763
|
-
if (value === null || value === void 0) {
|
|
9764
|
-
return null;
|
|
9765
|
-
}
|
|
9766
|
-
if (typeof value !== "string") {
|
|
9767
|
-
return null;
|
|
9768
|
-
}
|
|
9769
|
-
const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
|
|
9770
|
-
if (!host) {
|
|
9771
|
-
return null;
|
|
9772
|
-
}
|
|
9773
|
-
if (!isUsableLanIpv4(host)) {
|
|
9774
|
-
return null;
|
|
9775
|
-
}
|
|
9776
|
-
return host;
|
|
9777
|
-
}
|
|
9778
|
-
function isUsableLanIpv4(value) {
|
|
9779
|
-
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
|
|
9780
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
9781
|
-
return false;
|
|
9782
|
-
}
|
|
9783
|
-
const [first, second, , fourth] = parts;
|
|
9784
|
-
const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
9785
|
-
return privateRange && fourth !== 0 && fourth !== 255;
|
|
9786
|
-
}
|
|
9787
|
-
|
|
9788
10039
|
// src/hermes/session-title.ts
|
|
9789
10040
|
import { stat as stat7 } from "fs/promises";
|
|
9790
10041
|
import path13 from "path";
|
|
@@ -10247,7 +10498,7 @@ function stripCompressionTitleSuffix(value) {
|
|
|
10247
10498
|
}
|
|
10248
10499
|
|
|
10249
10500
|
// src/conversations/conversation-turns.ts
|
|
10250
|
-
import { randomUUID as
|
|
10501
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10251
10502
|
var MESSAGE_ORDER_STEP_MS = 10;
|
|
10252
10503
|
var ASSISTANT_ORDER_OFFSET_MS = 1;
|
|
10253
10504
|
function createAgentTurnDraft(input) {
|
|
@@ -10262,6 +10513,7 @@ function createAgentTurnDraft(input) {
|
|
|
10262
10513
|
conversation_id: input.manifest.id,
|
|
10263
10514
|
role: "user",
|
|
10264
10515
|
status: shouldQueue ? "queued" : "completed",
|
|
10516
|
+
run_id: runId,
|
|
10265
10517
|
client_message_id: input.idempotencyKey,
|
|
10266
10518
|
created_at: userCreatedAt,
|
|
10267
10519
|
updated_at: now,
|
|
@@ -10305,6 +10557,8 @@ function createAgentTurnDraft(input) {
|
|
|
10305
10557
|
profile_uid: input.runtime.profileUid,
|
|
10306
10558
|
profile_name_snapshot: input.runtime.profileName,
|
|
10307
10559
|
profile: input.runtime.profileName,
|
|
10560
|
+
owner_account_id: input.accountId,
|
|
10561
|
+
owner_app_instance_id: input.appInstanceId,
|
|
10308
10562
|
model: input.runtime.model,
|
|
10309
10563
|
provider: input.runtime.provider,
|
|
10310
10564
|
context_window: input.runtime.contextWindow
|
|
@@ -10481,10 +10735,10 @@ function createAssistantMessage(input) {
|
|
|
10481
10735
|
};
|
|
10482
10736
|
}
|
|
10483
10737
|
function createMessageId() {
|
|
10484
|
-
return `msg_${
|
|
10738
|
+
return `msg_${randomUUID8().replaceAll("-", "")}`;
|
|
10485
10739
|
}
|
|
10486
10740
|
function createRunId() {
|
|
10487
|
-
return `run_${
|
|
10741
|
+
return `run_${randomUUID8().replaceAll("-", "")}`;
|
|
10488
10742
|
}
|
|
10489
10743
|
function hasActiveOrQueuedRuns(snapshot) {
|
|
10490
10744
|
return snapshot.runs.some(
|
|
@@ -10496,6 +10750,9 @@ function validTimestampOrNow(value) {
|
|
|
10496
10750
|
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
10497
10751
|
}
|
|
10498
10752
|
|
|
10753
|
+
// src/conversations/queue-policy.ts
|
|
10754
|
+
var MAX_CONVERSATION_QUEUED_RUNS = 10;
|
|
10755
|
+
|
|
10499
10756
|
// src/conversations/conversation-orchestration.ts
|
|
10500
10757
|
var ConversationOrchestrationCoordinator = class {
|
|
10501
10758
|
constructor(deps) {
|
|
@@ -10526,6 +10783,47 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10526
10783
|
async appendCommandResult(input) {
|
|
10527
10784
|
return this.appendCommandResultLocked(input);
|
|
10528
10785
|
}
|
|
10786
|
+
async guideQueuedRun(conversationId, runId) {
|
|
10787
|
+
const result = await this.deps.withConversationLock(
|
|
10788
|
+
conversationId,
|
|
10789
|
+
() => this.guideQueuedRunLocked(conversationId, runId)
|
|
10790
|
+
);
|
|
10791
|
+
if (result.runningRunId) {
|
|
10792
|
+
void this.deps.runLifecycle.cancelRun(conversationId, result.runningRunId, {
|
|
10793
|
+
abortUpstream: true,
|
|
10794
|
+
reason: "interrupted by guided queued message"
|
|
10795
|
+
}).then((cancelResult) => {
|
|
10796
|
+
if (cancelResult.run.status === "cancelled") {
|
|
10797
|
+
return this.startNextQueuedRun(conversationId);
|
|
10798
|
+
}
|
|
10799
|
+
}).catch((error) => {
|
|
10800
|
+
void this.deps.logger.warn("conversation_guided_interrupt_failed", {
|
|
10801
|
+
conversation_id: conversationId,
|
|
10802
|
+
queued_run_id: runId,
|
|
10803
|
+
running_run_id: result.runningRunId,
|
|
10804
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10805
|
+
});
|
|
10806
|
+
});
|
|
10807
|
+
} else {
|
|
10808
|
+
void this.startNextQueuedRun(conversationId).catch((error) => {
|
|
10809
|
+
void this.deps.logger.warn("conversation_queue_drain_failed", {
|
|
10810
|
+
conversation_id: conversationId,
|
|
10811
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10812
|
+
});
|
|
10813
|
+
});
|
|
10814
|
+
}
|
|
10815
|
+
return {
|
|
10816
|
+
conversation_id: result.conversation_id,
|
|
10817
|
+
queued_run: result.queued_run,
|
|
10818
|
+
last_event_seq: result.last_event_seq
|
|
10819
|
+
};
|
|
10820
|
+
}
|
|
10821
|
+
async cancelQueuedRun(conversationId, runId) {
|
|
10822
|
+
return this.deps.withConversationLock(
|
|
10823
|
+
conversationId,
|
|
10824
|
+
() => this.cancelQueuedRunLocked(conversationId, runId)
|
|
10825
|
+
);
|
|
10826
|
+
}
|
|
10529
10827
|
startRunWorkerAndDrain(conversationId, runId, input) {
|
|
10530
10828
|
void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).catch(async (error) => {
|
|
10531
10829
|
if (isConversationNotFoundError(error)) {
|
|
@@ -10695,6 +10993,13 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10695
10993
|
}
|
|
10696
10994
|
}
|
|
10697
10995
|
const runtime = await readCurrentConversationRuntime(this.deps.paths, manifest);
|
|
10996
|
+
if ((hasRunningRuns(snapshot) || queuedRunCount(snapshot) > 0) && queuedRunCount(snapshot) >= MAX_CONVERSATION_QUEUED_RUNS) {
|
|
10997
|
+
throw new LinkHttpError(
|
|
10998
|
+
409,
|
|
10999
|
+
"conversation_queue_limit_reached",
|
|
11000
|
+
`conversation queue can contain at most ${MAX_CONVERSATION_QUEUED_RUNS} messages`
|
|
11001
|
+
);
|
|
11002
|
+
}
|
|
10698
11003
|
const { userMessage, assistantMessage, run, shouldQueue } = createAgentTurnDraft({
|
|
10699
11004
|
manifest,
|
|
10700
11005
|
snapshot,
|
|
@@ -10702,14 +11007,27 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10702
11007
|
content,
|
|
10703
11008
|
attachments: userAttachmentParts,
|
|
10704
11009
|
rawAttachments: input.attachments ?? [],
|
|
10705
|
-
idempotencyKey
|
|
11010
|
+
idempotencyKey,
|
|
11011
|
+
accountId: input.accountId,
|
|
11012
|
+
appInstanceId: input.appInstanceId
|
|
10706
11013
|
});
|
|
11014
|
+
if (input.accountId) {
|
|
11015
|
+
manifest = {
|
|
11016
|
+
...manifest,
|
|
11017
|
+
owner_account_id: input.accountId,
|
|
11018
|
+
owner_app_instance_id: input.appInstanceId,
|
|
11019
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
11020
|
+
};
|
|
11021
|
+
}
|
|
10707
11022
|
const assistantMessageId = run.assistant_message_id;
|
|
10708
11023
|
snapshot.messages.push(
|
|
10709
11024
|
...assistantMessage ? [userMessage, assistantMessage] : [userMessage]
|
|
10710
11025
|
);
|
|
10711
11026
|
snapshot.runs.push(run);
|
|
10712
11027
|
await this.deps.store.writeSnapshot(manifest.id, snapshot);
|
|
11028
|
+
if (input.accountId) {
|
|
11029
|
+
await this.deps.store.writeManifest(manifest);
|
|
11030
|
+
}
|
|
10713
11031
|
manifest = await this.deps.metadata.applyTemporaryTitleFromFirstMessage(
|
|
10714
11032
|
manifest,
|
|
10715
11033
|
snapshot,
|
|
@@ -10847,6 +11165,137 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10847
11165
|
return this.deps.commandHandlers.restartGatewayFromCommand(input);
|
|
10848
11166
|
}
|
|
10849
11167
|
}
|
|
11168
|
+
async guideQueuedRunLocked(conversationId, runId) {
|
|
11169
|
+
const manifest = await this.deps.store.readRunnableManifest(conversationId);
|
|
11170
|
+
const snapshot = await this.deps.store.readSnapshot(conversationId);
|
|
11171
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
11172
|
+
if (!run) {
|
|
11173
|
+
throw new LinkHttpError(404, "run_not_found", "Run was not found");
|
|
11174
|
+
}
|
|
11175
|
+
if (run.status === "running") {
|
|
11176
|
+
throw new LinkHttpError(
|
|
11177
|
+
409,
|
|
11178
|
+
"queued_run_already_started",
|
|
11179
|
+
"Queued run has already started"
|
|
11180
|
+
);
|
|
11181
|
+
}
|
|
11182
|
+
if (run.status !== "queued") {
|
|
11183
|
+
throw new LinkHttpError(
|
|
11184
|
+
409,
|
|
11185
|
+
"queued_run_not_active",
|
|
11186
|
+
"Queued run is no longer active"
|
|
11187
|
+
);
|
|
11188
|
+
}
|
|
11189
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11190
|
+
const runningRun = snapshot.runs.find((item) => item.status === "running");
|
|
11191
|
+
run.queue_mode = "guided_interrupt";
|
|
11192
|
+
run.queue_promoted_at = now;
|
|
11193
|
+
if (runningRun) {
|
|
11194
|
+
run.guided_after_run_id = runningRun.id;
|
|
11195
|
+
}
|
|
11196
|
+
const user = snapshot.messages.find(
|
|
11197
|
+
(message) => message.id === run.trigger_message_id
|
|
11198
|
+
);
|
|
11199
|
+
if (user) {
|
|
11200
|
+
user.updated_at = now;
|
|
11201
|
+
user.hermes = {
|
|
11202
|
+
...user.hermes ?? {},
|
|
11203
|
+
queue_mode: "guided_interrupt",
|
|
11204
|
+
queue_promoted_at: now,
|
|
11205
|
+
...runningRun ? { guided_after_run_id: runningRun.id } : {}
|
|
11206
|
+
};
|
|
11207
|
+
}
|
|
11208
|
+
const currentIndex = snapshot.runs.findIndex((item) => item.id === run.id);
|
|
11209
|
+
if (currentIndex >= 0) {
|
|
11210
|
+
snapshot.runs.splice(currentIndex, 1);
|
|
11211
|
+
const firstQueuedIndex = snapshot.runs.findIndex(
|
|
11212
|
+
(item) => item.status === "queued"
|
|
11213
|
+
);
|
|
11214
|
+
snapshot.runs.splice(
|
|
11215
|
+
firstQueuedIndex === -1 ? snapshot.runs.length : firstQueuedIndex,
|
|
11216
|
+
0,
|
|
11217
|
+
run
|
|
11218
|
+
);
|
|
11219
|
+
}
|
|
11220
|
+
await this.deps.store.writeSnapshot(conversationId, snapshot);
|
|
11221
|
+
const event = await this.deps.appendEvent(conversationId, {
|
|
11222
|
+
type: "run.queue_promoted",
|
|
11223
|
+
message_id: user?.id,
|
|
11224
|
+
run_id: run.id,
|
|
11225
|
+
payload: {
|
|
11226
|
+
run,
|
|
11227
|
+
queued_run: run,
|
|
11228
|
+
mode: "guided_interrupt",
|
|
11229
|
+
guided_after_run_id: runningRun?.id
|
|
11230
|
+
}
|
|
11231
|
+
});
|
|
11232
|
+
await this.deps.metadata.persistConversationStats(conversationId, snapshot);
|
|
11233
|
+
return {
|
|
11234
|
+
conversation_id: manifest.id,
|
|
11235
|
+
queued_run: { id: run.id, status: run.status },
|
|
11236
|
+
last_event_seq: event.seq,
|
|
11237
|
+
...runningRun ? { runningRunId: runningRun.id } : {}
|
|
11238
|
+
};
|
|
11239
|
+
}
|
|
11240
|
+
async cancelQueuedRunLocked(conversationId, runId) {
|
|
11241
|
+
const manifest = await this.deps.store.readRunnableManifest(conversationId);
|
|
11242
|
+
const snapshot = await this.deps.store.readSnapshot(conversationId);
|
|
11243
|
+
const run = snapshot.runs.find((item) => item.id === runId);
|
|
11244
|
+
if (!run) {
|
|
11245
|
+
throw new LinkHttpError(404, "run_not_found", "Run was not found");
|
|
11246
|
+
}
|
|
11247
|
+
if (run.status === "running") {
|
|
11248
|
+
throw new LinkHttpError(
|
|
11249
|
+
409,
|
|
11250
|
+
"queued_run_already_started",
|
|
11251
|
+
"Queued run has already started"
|
|
11252
|
+
);
|
|
11253
|
+
}
|
|
11254
|
+
if (run.status !== "queued") {
|
|
11255
|
+
return {
|
|
11256
|
+
conversation_id: manifest.id,
|
|
11257
|
+
queued_run: { id: run.id, status: run.status },
|
|
11258
|
+
last_event_seq: manifest.last_event_seq
|
|
11259
|
+
};
|
|
11260
|
+
}
|
|
11261
|
+
const cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11262
|
+
run.status = "cancelled";
|
|
11263
|
+
run.completed_at = cancelledAt;
|
|
11264
|
+
run.error_message = "cancelled while queued";
|
|
11265
|
+
const user = snapshot.messages.find(
|
|
11266
|
+
(message) => message.id === run.trigger_message_id
|
|
11267
|
+
);
|
|
11268
|
+
if (user) {
|
|
11269
|
+
user.status = "cancelled";
|
|
11270
|
+
user.updated_at = cancelledAt;
|
|
11271
|
+
user.hermes = {
|
|
11272
|
+
...user.hermes ?? {},
|
|
11273
|
+
queue_cancelled_at: cancelledAt
|
|
11274
|
+
};
|
|
11275
|
+
}
|
|
11276
|
+
await this.deps.store.writeSnapshot(conversationId, snapshot);
|
|
11277
|
+
let eventSeq = manifest.last_event_seq;
|
|
11278
|
+
if (user) {
|
|
11279
|
+
const userEvent = await this.deps.appendEvent(conversationId, {
|
|
11280
|
+
type: "message.completed",
|
|
11281
|
+
message_id: user.id,
|
|
11282
|
+
run_id: run.id,
|
|
11283
|
+
payload: { message: user, cancelled: true }
|
|
11284
|
+
});
|
|
11285
|
+
eventSeq = userEvent.seq;
|
|
11286
|
+
}
|
|
11287
|
+
const runEvent = await this.deps.appendEvent(conversationId, {
|
|
11288
|
+
type: "run.cancelled",
|
|
11289
|
+
run_id: run.id,
|
|
11290
|
+
payload: { run, reason: "cancelled while queued" }
|
|
11291
|
+
});
|
|
11292
|
+
await this.deps.metadata.persistConversationStats(conversationId, snapshot);
|
|
11293
|
+
return {
|
|
11294
|
+
conversation_id: manifest.id,
|
|
11295
|
+
queued_run: { id: run.id, status: run.status },
|
|
11296
|
+
last_event_seq: Math.max(eventSeq, runEvent.seq)
|
|
11297
|
+
};
|
|
11298
|
+
}
|
|
10850
11299
|
async resetConversationContextLocked(input) {
|
|
10851
11300
|
if (hasRunningRuns(input.snapshot)) {
|
|
10852
11301
|
return this.appendCommandResultLocked({
|
|
@@ -11133,7 +11582,7 @@ function projectAgentEvent(input) {
|
|
|
11133
11582
|
summary,
|
|
11134
11583
|
args
|
|
11135
11584
|
});
|
|
11136
|
-
const detail = status === "failed" ?
|
|
11585
|
+
const detail = status === "failed" ? readErrorMessage2(input.payload) ?? actionSummary ?? void 0 : actionSummary ?? void 0;
|
|
11137
11586
|
const subtitle = actionSummary ?? (status === "running" ? `\u6B63\u5728\u8C03\u7528 ${name}` : status === "completed" ? `${name} \u5DF2\u5B8C\u6210` : `${name} \u6267\u884C\u5931\u8D25`);
|
|
11138
11587
|
return {
|
|
11139
11588
|
id,
|
|
@@ -11430,7 +11879,7 @@ function stableStringify(value) {
|
|
|
11430
11879
|
function hashAgentEventKey(value) {
|
|
11431
11880
|
return createHash3("sha256").update(value).digest("hex").slice(0, 16);
|
|
11432
11881
|
}
|
|
11433
|
-
function
|
|
11882
|
+
function readErrorMessage2(payload) {
|
|
11434
11883
|
if (typeof payload.error === "string" && payload.error.trim()) {
|
|
11435
11884
|
return payload.error.trim();
|
|
11436
11885
|
}
|
|
@@ -11785,7 +12234,7 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
|
11785
12234
|
// src/conversations/conversation-store.ts
|
|
11786
12235
|
import {
|
|
11787
12236
|
appendFile as appendFile2,
|
|
11788
|
-
mkdir as
|
|
12237
|
+
mkdir as mkdir10,
|
|
11789
12238
|
readdir as readdir5,
|
|
11790
12239
|
readFile as readFile7,
|
|
11791
12240
|
rm as rm5,
|
|
@@ -11798,7 +12247,7 @@ var ConversationStore = class {
|
|
|
11798
12247
|
}
|
|
11799
12248
|
paths;
|
|
11800
12249
|
async ensureConversationsDir() {
|
|
11801
|
-
await
|
|
12250
|
+
await mkdir10(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
11802
12251
|
}
|
|
11803
12252
|
async listConversationIds() {
|
|
11804
12253
|
await this.ensureConversationsDir();
|
|
@@ -11813,7 +12262,7 @@ var ConversationStore = class {
|
|
|
11813
12262
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
11814
12263
|
}
|
|
11815
12264
|
async createConversation(manifest, snapshot = createEmptySnapshot2()) {
|
|
11816
|
-
await
|
|
12265
|
+
await mkdir10(this.conversationDir(manifest.id), {
|
|
11817
12266
|
recursive: true,
|
|
11818
12267
|
mode: 448
|
|
11819
12268
|
});
|
|
@@ -11853,7 +12302,7 @@ var ConversationStore = class {
|
|
|
11853
12302
|
conversation_id: conversationId,
|
|
11854
12303
|
created_at: now
|
|
11855
12304
|
};
|
|
11856
|
-
await
|
|
12305
|
+
await mkdir10(this.conversationDir(conversationId), {
|
|
11857
12306
|
recursive: true,
|
|
11858
12307
|
mode: 448
|
|
11859
12308
|
});
|
|
@@ -11949,7 +12398,7 @@ function isNodeError9(error, code) {
|
|
|
11949
12398
|
}
|
|
11950
12399
|
|
|
11951
12400
|
// src/conversations/hermes-session-sync.ts
|
|
11952
|
-
import { randomUUID as
|
|
12401
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
11953
12402
|
import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
|
|
11954
12403
|
import path16 from "path";
|
|
11955
12404
|
|
|
@@ -12391,7 +12840,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12391
12840
|
result.skipped_existing += 1;
|
|
12392
12841
|
continue;
|
|
12393
12842
|
}
|
|
12394
|
-
const
|
|
12843
|
+
const importedConversationId = await importHermesSession({
|
|
12395
12844
|
paths,
|
|
12396
12845
|
store,
|
|
12397
12846
|
logger,
|
|
@@ -12401,9 +12850,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12401
12850
|
profile: candidate.profileName,
|
|
12402
12851
|
message: error instanceof Error ? error.message : String(error)
|
|
12403
12852
|
});
|
|
12404
|
-
return
|
|
12853
|
+
return null;
|
|
12405
12854
|
});
|
|
12406
|
-
if (
|
|
12855
|
+
if (importedConversationId) {
|
|
12407
12856
|
result.imported_count += 1;
|
|
12408
12857
|
for (const sessionId of candidateSessionIds) {
|
|
12409
12858
|
knownHermesSessions.sessionIds.add(sessionId);
|
|
@@ -12417,6 +12866,80 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12417
12866
|
}
|
|
12418
12867
|
return result;
|
|
12419
12868
|
}
|
|
12869
|
+
async function syncHermesCronSessionIntoConversations(paths, logger, input) {
|
|
12870
|
+
const jobId = input.jobId.trim();
|
|
12871
|
+
if (!jobId) {
|
|
12872
|
+
return { conversation_id: null, imported: false, reprojected: false };
|
|
12873
|
+
}
|
|
12874
|
+
const store = new ConversationStore(paths);
|
|
12875
|
+
const knownHermesSessions = await readKnownHermesSessions(store);
|
|
12876
|
+
const profileName = input.profileName.trim() || DEFAULT_PROFILE_NAME;
|
|
12877
|
+
const profileDir = resolveHermesProfileDir(profileName);
|
|
12878
|
+
const dbPath = path16.join(profileDir, "state.db");
|
|
12879
|
+
const sessions = await listProfileSessionsByIdPrefix(
|
|
12880
|
+
dbPath,
|
|
12881
|
+
`cron_${jobId}_`
|
|
12882
|
+
).catch((error) => {
|
|
12883
|
+
void logger.warn("hermes_cron_session_query_failed", {
|
|
12884
|
+
profile: profileName,
|
|
12885
|
+
job_id: jobId,
|
|
12886
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12887
|
+
});
|
|
12888
|
+
return [];
|
|
12889
|
+
});
|
|
12890
|
+
const candidate = selectCronSessionCandidate(
|
|
12891
|
+
sessions.filter((session) => !isDeletedSession(session) && !isHiddenSession(session)).map((session) => ({ profileName, profileDir, dbPath, session })),
|
|
12892
|
+
input.runAt
|
|
12893
|
+
);
|
|
12894
|
+
if (!candidate) {
|
|
12895
|
+
return { conversation_id: null, imported: false, reprojected: false };
|
|
12896
|
+
}
|
|
12897
|
+
const knownConversationIds = findKnownConversationIdsForCandidate(
|
|
12898
|
+
knownHermesSessions,
|
|
12899
|
+
candidate
|
|
12900
|
+
);
|
|
12901
|
+
if (knownConversationIds.length > 0) {
|
|
12902
|
+
const reprojected = await mergeExistingHermesConversation({
|
|
12903
|
+
paths,
|
|
12904
|
+
store,
|
|
12905
|
+
logger,
|
|
12906
|
+
candidate,
|
|
12907
|
+
conversationIds: knownConversationIds
|
|
12908
|
+
});
|
|
12909
|
+
const canonical = await findCanonicalConversationForCandidate(
|
|
12910
|
+
store,
|
|
12911
|
+
knownConversationIds
|
|
12912
|
+
);
|
|
12913
|
+
return {
|
|
12914
|
+
conversation_id: canonical?.conversationId ?? knownConversationIds[0] ?? null,
|
|
12915
|
+
hermes_session_id: candidate.session.id,
|
|
12916
|
+
imported: false,
|
|
12917
|
+
reprojected
|
|
12918
|
+
};
|
|
12919
|
+
}
|
|
12920
|
+
if (lineageSessionIds(candidate).some(
|
|
12921
|
+
(sessionId) => knownHermesSessions.sessionIds.has(sessionId)
|
|
12922
|
+
)) {
|
|
12923
|
+
return {
|
|
12924
|
+
conversation_id: null,
|
|
12925
|
+
hermes_session_id: candidate.session.id,
|
|
12926
|
+
imported: false,
|
|
12927
|
+
reprojected: false
|
|
12928
|
+
};
|
|
12929
|
+
}
|
|
12930
|
+
const conversationId = await importHermesSession({
|
|
12931
|
+
paths,
|
|
12932
|
+
store,
|
|
12933
|
+
logger,
|
|
12934
|
+
candidate
|
|
12935
|
+
});
|
|
12936
|
+
return {
|
|
12937
|
+
conversation_id: conversationId,
|
|
12938
|
+
hermes_session_id: candidate.session.id,
|
|
12939
|
+
imported: conversationId != null,
|
|
12940
|
+
reprojected: false
|
|
12941
|
+
};
|
|
12942
|
+
}
|
|
12420
12943
|
async function importHermesSession(input) {
|
|
12421
12944
|
const { paths, store, logger, candidate } = input;
|
|
12422
12945
|
const profile = await resolveConversationProfileTarget(
|
|
@@ -12442,7 +12965,7 @@ async function importHermesSession(input) {
|
|
|
12442
12965
|
}),
|
|
12443
12966
|
runs: []
|
|
12444
12967
|
};
|
|
12445
|
-
const title =
|
|
12968
|
+
const title = deriveHermesConversationTitle(candidate, snapshot);
|
|
12446
12969
|
const importedStats = buildImportedHermesStats({
|
|
12447
12970
|
candidate,
|
|
12448
12971
|
snapshot,
|
|
@@ -12509,7 +13032,7 @@ async function importHermesSession(input) {
|
|
|
12509
13032
|
paths,
|
|
12510
13033
|
toStatsIndexRecord(await store.readManifest(conversationId), stats)
|
|
12511
13034
|
);
|
|
12512
|
-
return
|
|
13035
|
+
return conversationId;
|
|
12513
13036
|
}
|
|
12514
13037
|
async function mergeExistingHermesConversation(input) {
|
|
12515
13038
|
const conversations = await readExistingHermesConversations(
|
|
@@ -12618,7 +13141,17 @@ function lineageSessionIds(candidate) {
|
|
|
12618
13141
|
]);
|
|
12619
13142
|
}
|
|
12620
13143
|
function lineageTitle(candidate) {
|
|
12621
|
-
|
|
13144
|
+
const explicitLineageTitle = normalizeOptionalTitle(
|
|
13145
|
+
candidate.session._lineage_title
|
|
13146
|
+
);
|
|
13147
|
+
if (explicitLineageTitle) {
|
|
13148
|
+
return explicitLineageTitle;
|
|
13149
|
+
}
|
|
13150
|
+
const sessionTitle = readString9(candidate.session, "title") ?? "";
|
|
13151
|
+
return isDefaultConversationTitle(sessionTitle) ? void 0 : stripCompressionTitleSuffix2(sessionTitle);
|
|
13152
|
+
}
|
|
13153
|
+
function deriveHermesConversationTitle(candidate, snapshot) {
|
|
13154
|
+
return lineageTitle(candidate) ?? normalizeHermesSourceTitle(readString9(candidate.session, "title")) ?? firstUserText(snapshot);
|
|
12622
13155
|
}
|
|
12623
13156
|
function lineageManifestPatch(candidate) {
|
|
12624
13157
|
const sessionIds = lineageSessionIds(candidate);
|
|
@@ -12771,7 +13304,7 @@ function mergeHermesLineageIntoManifest(input) {
|
|
|
12771
13304
|
profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
|
|
12772
13305
|
updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
|
|
12773
13306
|
};
|
|
12774
|
-
const title =
|
|
13307
|
+
const title = deriveHermesConversationTitle(input.candidate, input.snapshot);
|
|
12775
13308
|
if (title && canSyncHermesTitle(input.manifest)) {
|
|
12776
13309
|
nextBase.title = normalizeTitle(title);
|
|
12777
13310
|
nextBase.title_source = "hermes";
|
|
@@ -13338,6 +13871,15 @@ async function discoverHermesProfileNames() {
|
|
|
13338
13871
|
});
|
|
13339
13872
|
}
|
|
13340
13873
|
async function listProfileSessions(dbPath) {
|
|
13874
|
+
return listProfileSessionsWhere(dbPath);
|
|
13875
|
+
}
|
|
13876
|
+
async function listProfileSessionsByIdPrefix(dbPath, sessionIdPrefix) {
|
|
13877
|
+
return listProfileSessionsWhere(dbPath, {
|
|
13878
|
+
whereSql: "WHERE s.id LIKE ?",
|
|
13879
|
+
params: [`${sessionIdPrefix}%`]
|
|
13880
|
+
});
|
|
13881
|
+
}
|
|
13882
|
+
async function listProfileSessionsWhere(dbPath, filter = null) {
|
|
13341
13883
|
if (!await isFile(dbPath)) {
|
|
13342
13884
|
return [];
|
|
13343
13885
|
}
|
|
@@ -13362,14 +13904,54 @@ async function listProfileSessions(dbPath) {
|
|
|
13362
13904
|
`
|
|
13363
13905
|
SELECT ${selectColumns}, ${lastActiveSql}
|
|
13364
13906
|
FROM sessions s
|
|
13907
|
+
${filter?.whereSql ?? ""}
|
|
13365
13908
|
ORDER BY last_active DESC
|
|
13366
13909
|
`
|
|
13367
|
-
).all();
|
|
13910
|
+
).all(...filter?.params ?? []);
|
|
13368
13911
|
return projectCompressionTips(rows);
|
|
13369
13912
|
} finally {
|
|
13370
13913
|
db?.close();
|
|
13371
13914
|
}
|
|
13372
13915
|
}
|
|
13916
|
+
function selectCronSessionCandidate(candidates, runAt) {
|
|
13917
|
+
if (candidates.length === 0) {
|
|
13918
|
+
return null;
|
|
13919
|
+
}
|
|
13920
|
+
const targetTime = Date.parse(runAt ?? "");
|
|
13921
|
+
return [...candidates].sort((left, right) => {
|
|
13922
|
+
const leftTime = cronCandidateTime(left);
|
|
13923
|
+
const rightTime = cronCandidateTime(right);
|
|
13924
|
+
if (!Number.isNaN(targetTime)) {
|
|
13925
|
+
const leftFuture = leftTime > targetTime + 6e4 ? 1 : 0;
|
|
13926
|
+
const rightFuture = rightTime > targetTime + 6e4 ? 1 : 0;
|
|
13927
|
+
if (leftFuture !== rightFuture) {
|
|
13928
|
+
return leftFuture - rightFuture;
|
|
13929
|
+
}
|
|
13930
|
+
const leftDistance = Number.isNaN(leftTime) ? Number.POSITIVE_INFINITY : Math.abs(leftTime - targetTime);
|
|
13931
|
+
const rightDistance = Number.isNaN(rightTime) ? Number.POSITIVE_INFINITY : Math.abs(rightTime - targetTime);
|
|
13932
|
+
if (leftDistance !== rightDistance) {
|
|
13933
|
+
return leftDistance - rightDistance;
|
|
13934
|
+
}
|
|
13935
|
+
}
|
|
13936
|
+
return rightTime - leftTime;
|
|
13937
|
+
})[0] ?? null;
|
|
13938
|
+
}
|
|
13939
|
+
function cronCandidateTime(candidate) {
|
|
13940
|
+
return hermesTimestampMs(candidate.session.last_active) ?? hermesTimestampMs(candidate.session.ended_at) ?? hermesTimestampMs(candidate.session.started_at) ?? Number.NaN;
|
|
13941
|
+
}
|
|
13942
|
+
function hermesTimestampMs(value) {
|
|
13943
|
+
const seconds = readNumber2(value);
|
|
13944
|
+
return seconds == null ? null : seconds * 1e3;
|
|
13945
|
+
}
|
|
13946
|
+
async function findCanonicalConversationForCandidate(store, conversationIds) {
|
|
13947
|
+
const conversations = await readExistingHermesConversations(
|
|
13948
|
+
store,
|
|
13949
|
+
conversationIds
|
|
13950
|
+
);
|
|
13951
|
+
return selectCanonicalHermesConversation(
|
|
13952
|
+
conversations.filter((item) => item.manifest.status === "active")
|
|
13953
|
+
);
|
|
13954
|
+
}
|
|
13373
13955
|
function appendHermesRawMessage(message, row) {
|
|
13374
13956
|
const rows = readHermesRawMessageRows(message.raw);
|
|
13375
13957
|
message.raw = rows.length === 0 ? {
|
|
@@ -13719,7 +14301,7 @@ function toLinkMessage(input) {
|
|
|
13719
14301
|
const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
|
|
13720
14302
|
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
13721
14303
|
return {
|
|
13722
|
-
id: `msg_${
|
|
14304
|
+
id: `msg_${randomUUID9().replaceAll("-", "")}`,
|
|
13723
14305
|
schema_version: 1,
|
|
13724
14306
|
conversation_id: input.conversationId,
|
|
13725
14307
|
role,
|
|
@@ -13771,6 +14353,17 @@ function normalizeTitle(value) {
|
|
|
13771
14353
|
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
13772
14354
|
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
13773
14355
|
}
|
|
14356
|
+
function normalizeOptionalTitle(value) {
|
|
14357
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
14358
|
+
return normalized ? normalized : void 0;
|
|
14359
|
+
}
|
|
14360
|
+
function normalizeHermesSourceTitle(value) {
|
|
14361
|
+
const normalized = normalizeOptionalTitle(value);
|
|
14362
|
+
if (!normalized || isDefaultConversationTitle(normalized)) {
|
|
14363
|
+
return void 0;
|
|
14364
|
+
}
|
|
14365
|
+
return normalized;
|
|
14366
|
+
}
|
|
13774
14367
|
function canSyncHermesTitle(manifest) {
|
|
13775
14368
|
return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
|
|
13776
14369
|
}
|
|
@@ -13891,7 +14484,7 @@ async function isFile(filePath) {
|
|
|
13891
14484
|
});
|
|
13892
14485
|
}
|
|
13893
14486
|
function createConversationId() {
|
|
13894
|
-
return `conv_${
|
|
14487
|
+
return `conv_${randomUUID9().replaceAll("-", "")}`;
|
|
13895
14488
|
}
|
|
13896
14489
|
function isoFromHermesTime(value) {
|
|
13897
14490
|
const numeric = readNumber2(value);
|
|
@@ -15290,71 +15883,6 @@ function compactProcessOutput(value) {
|
|
|
15290
15883
|
return compact || null;
|
|
15291
15884
|
}
|
|
15292
15885
|
|
|
15293
|
-
// src/identity/identity.ts
|
|
15294
|
-
import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
|
|
15295
|
-
import { mkdir as mkdir10, chmod as chmod2 } from "fs/promises";
|
|
15296
|
-
import { z } from "zod";
|
|
15297
|
-
var linkIdentitySchema = z.object({
|
|
15298
|
-
install_id: z.string().min(1),
|
|
15299
|
-
link_id: z.string().min(1).nullable().optional(),
|
|
15300
|
-
public_key_pem: z.string().min(1),
|
|
15301
|
-
private_key_pem: z.string().min(1),
|
|
15302
|
-
created_at: z.string().min(1),
|
|
15303
|
-
updated_at: z.string().min(1)
|
|
15304
|
-
});
|
|
15305
|
-
async function loadIdentity(paths = resolveRuntimePaths()) {
|
|
15306
|
-
const value = await readJsonFile(paths.identityFile);
|
|
15307
|
-
if (value === null) {
|
|
15308
|
-
return null;
|
|
15309
|
-
}
|
|
15310
|
-
return linkIdentitySchema.parse(value);
|
|
15311
|
-
}
|
|
15312
|
-
async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
15313
|
-
const existing = await loadIdentity(paths);
|
|
15314
|
-
if (existing) {
|
|
15315
|
-
return existing;
|
|
15316
|
-
}
|
|
15317
|
-
await mkdir10(paths.homeDir, { recursive: true, mode: 448 });
|
|
15318
|
-
await chmod2(paths.homeDir, 448).catch(() => void 0);
|
|
15319
|
-
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
15320
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15321
|
-
const identity = {
|
|
15322
|
-
install_id: `install_${randomUUID9().replaceAll("-", "")}`,
|
|
15323
|
-
link_id: null,
|
|
15324
|
-
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
15325
|
-
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
15326
|
-
created_at: now,
|
|
15327
|
-
updated_at: now
|
|
15328
|
-
};
|
|
15329
|
-
await writeJsonFile(paths.identityFile, identity);
|
|
15330
|
-
return identity;
|
|
15331
|
-
}
|
|
15332
|
-
async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
|
|
15333
|
-
const identity = await ensureIdentity(paths);
|
|
15334
|
-
const next = {
|
|
15335
|
-
...identity,
|
|
15336
|
-
link_id: linkId,
|
|
15337
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15338
|
-
};
|
|
15339
|
-
await writeJsonFile(paths.identityFile, next);
|
|
15340
|
-
return next;
|
|
15341
|
-
}
|
|
15342
|
-
function signRelayNonce(identity, nonce) {
|
|
15343
|
-
return signIdentityPayload(identity, nonce);
|
|
15344
|
-
}
|
|
15345
|
-
function signIdentityPayload(identity, payload) {
|
|
15346
|
-
const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
|
|
15347
|
-
return signature.toString("base64url");
|
|
15348
|
-
}
|
|
15349
|
-
function getIdentityStatus(identity) {
|
|
15350
|
-
return {
|
|
15351
|
-
installId: identity.install_id,
|
|
15352
|
-
linkId: identity.link_id ?? null,
|
|
15353
|
-
hasPrivateKey: identity.private_key_pem.trim().length > 0,
|
|
15354
|
-
publicKeyPem: identity.public_key_pem
|
|
15355
|
-
};
|
|
15356
|
-
}
|
|
15357
|
-
|
|
15358
15886
|
// src/conversations/hermes-sse.ts
|
|
15359
15887
|
async function* parseSseResponse(response) {
|
|
15360
15888
|
if (!response.body) {
|
|
@@ -16036,7 +16564,7 @@ function normalizeHermesStreamEvent(event) {
|
|
|
16036
16564
|
...event.payload,
|
|
16037
16565
|
type: "run.failed",
|
|
16038
16566
|
error: {
|
|
16039
|
-
message:
|
|
16567
|
+
message: readErrorMessage3(event.payload) ?? readDelta(event.payload) ?? "Hermes run failed"
|
|
16040
16568
|
}
|
|
16041
16569
|
}
|
|
16042
16570
|
};
|
|
@@ -16112,11 +16640,24 @@ function normalizeHermesResponseEvent(event) {
|
|
|
16112
16640
|
} : null;
|
|
16113
16641
|
}
|
|
16114
16642
|
case "response.created":
|
|
16115
|
-
return
|
|
16643
|
+
return normalizeResponseCreated(event);
|
|
16116
16644
|
default:
|
|
16117
16645
|
return null;
|
|
16118
16646
|
}
|
|
16119
16647
|
}
|
|
16648
|
+
function normalizeResponseCreated(event) {
|
|
16649
|
+
const response = toRecord12(event.payload.response ?? event.payload);
|
|
16650
|
+
const responseId = readString14(response, "id") ?? readString14(event.payload, "id");
|
|
16651
|
+
return responseId ? {
|
|
16652
|
+
...event,
|
|
16653
|
+
payloadType: "response.created",
|
|
16654
|
+
payload: {
|
|
16655
|
+
type: "response.created",
|
|
16656
|
+
response_id: responseId,
|
|
16657
|
+
response
|
|
16658
|
+
}
|
|
16659
|
+
} : null;
|
|
16660
|
+
}
|
|
16120
16661
|
function normalizeResponseOutputItemAdded(event) {
|
|
16121
16662
|
const item = toRecord12(event.payload.item);
|
|
16122
16663
|
if (readString14(item, "type") !== "function_call") {
|
|
@@ -16213,7 +16754,7 @@ function normalizeStreamingTextDelta(currentText, nextChunk) {
|
|
|
16213
16754
|
}
|
|
16214
16755
|
return nextChunk;
|
|
16215
16756
|
}
|
|
16216
|
-
function
|
|
16757
|
+
function readErrorMessage3(payload) {
|
|
16217
16758
|
if (typeof payload.error === "string" && payload.error.trim()) {
|
|
16218
16759
|
return payload.error.trim();
|
|
16219
16760
|
}
|
|
@@ -16277,7 +16818,7 @@ function isTopLevelErrorEvent(event) {
|
|
|
16277
16818
|
if (type.startsWith("tool.")) {
|
|
16278
16819
|
return false;
|
|
16279
16820
|
}
|
|
16280
|
-
return type === "error" || type === "run.error" || event.eventName === "error" || Boolean(
|
|
16821
|
+
return type === "error" || type === "run.error" || event.eventName === "error" || Boolean(readErrorMessage3(event.payload));
|
|
16281
16822
|
}
|
|
16282
16823
|
function readChatCompletionDelta(payload) {
|
|
16283
16824
|
const choice = readFirstChoice(payload);
|
|
@@ -16863,12 +17404,27 @@ var ConversationRunLifecycle = class {
|
|
|
16863
17404
|
this.deps.scheduleTitleRefresh(input.conversationId);
|
|
16864
17405
|
}
|
|
16865
17406
|
async handleNormalizedHermesEvent(input) {
|
|
17407
|
+
if (input.event.payloadType === "response.created") {
|
|
17408
|
+
const responseId = readResponseId(input.event.payload);
|
|
17409
|
+
if (responseId) {
|
|
17410
|
+
await this.updateRun(input.conversationId, input.runId, {
|
|
17411
|
+
hermes_response_id: responseId
|
|
17412
|
+
});
|
|
17413
|
+
}
|
|
17414
|
+
await this.persistHermesEvent(input.conversationId, input.runId, input.event);
|
|
17415
|
+
return false;
|
|
17416
|
+
}
|
|
16866
17417
|
if (input.event.payloadType === "run.completed") {
|
|
16867
17418
|
if (input.cronJobIdsBeforeRun) {
|
|
17419
|
+
const snapshot = await this.deps.readSnapshot(input.conversationId).catch(() => null);
|
|
17420
|
+
const run = snapshot?.runs.find((item) => item.id === input.runId) ?? null;
|
|
17421
|
+
const manifest = await this.deps.readActiveManifest(input.conversationId).catch(() => null);
|
|
16868
17422
|
await this.bindNewCronJobsCreatedByRun({
|
|
16869
17423
|
profileName: input.profileName,
|
|
16870
17424
|
conversationId: input.conversationId,
|
|
16871
|
-
beforeJobIds: input.cronJobIdsBeforeRun
|
|
17425
|
+
beforeJobIds: input.cronJobIdsBeforeRun,
|
|
17426
|
+
ownerAccountId: run?.owner_account_id ?? manifest?.owner_account_id,
|
|
17427
|
+
ownerAppInstanceId: run?.owner_app_instance_id ?? manifest?.owner_app_instance_id
|
|
16872
17428
|
});
|
|
16873
17429
|
}
|
|
16874
17430
|
await this.deps.syncCronDeliveries().catch((error) => {
|
|
@@ -16926,7 +17482,7 @@ var ConversationRunLifecycle = class {
|
|
|
16926
17482
|
await this.failRun(
|
|
16927
17483
|
input.conversationId,
|
|
16928
17484
|
input.runId,
|
|
16929
|
-
|
|
17485
|
+
readErrorMessage3(input.event.payload) ?? "Hermes run failed",
|
|
16930
17486
|
input.event
|
|
16931
17487
|
);
|
|
16932
17488
|
return true;
|
|
@@ -17176,8 +17732,11 @@ var ConversationRunLifecycle = class {
|
|
|
17176
17732
|
const userMessage = input.snapshot.messages.find(
|
|
17177
17733
|
(message) => message.id === input.run.trigger_message_id
|
|
17178
17734
|
);
|
|
17735
|
+
const prefix = guidedInterruptInputPrefix(input.run);
|
|
17179
17736
|
if (!userMessage || !userMessage.parts.some(isVoicePart)) {
|
|
17180
|
-
return
|
|
17737
|
+
return prefix ? `${prefix}
|
|
17738
|
+
|
|
17739
|
+
${input.fallbackInput}` : input.fallbackInput;
|
|
17181
17740
|
}
|
|
17182
17741
|
const content = messageText(userMessage);
|
|
17183
17742
|
const voiceLines = [];
|
|
@@ -17209,11 +17768,16 @@ ${attachmentLines.join("\n")}`
|
|
|
17209
17768
|
);
|
|
17210
17769
|
}
|
|
17211
17770
|
if (sections.length === 0) {
|
|
17212
|
-
return
|
|
17771
|
+
return prefix ? `${prefix}
|
|
17772
|
+
|
|
17773
|
+
${content}` : content;
|
|
17213
17774
|
}
|
|
17214
|
-
|
|
17775
|
+
const resolved = `${content ? `${content}
|
|
17215
17776
|
|
|
17216
17777
|
` : ""}${sections.join("\n\n")}`;
|
|
17778
|
+
return prefix ? `${prefix}
|
|
17779
|
+
|
|
17780
|
+
${resolved}` : resolved;
|
|
17217
17781
|
}
|
|
17218
17782
|
async updateRun(conversationId, runId, patch) {
|
|
17219
17783
|
return this.deps.withConversationLock(
|
|
@@ -17634,6 +18198,13 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17634
18198
|
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
17635
18199
|
});
|
|
17636
18200
|
await this.deps.persistConversationStats(conversationId, snapshot);
|
|
18201
|
+
void this.reportRunNotification({
|
|
18202
|
+
conversationId,
|
|
18203
|
+
run,
|
|
18204
|
+
assistant,
|
|
18205
|
+
eventKind: "run_completed",
|
|
18206
|
+
occurredAt: completedAt
|
|
18207
|
+
});
|
|
17637
18208
|
}
|
|
17638
18209
|
async failRunLocked(conversationId, runId, message, source) {
|
|
17639
18210
|
const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
|
|
@@ -17647,7 +18218,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17647
18218
|
run.status = "failed";
|
|
17648
18219
|
run.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17649
18220
|
run.error_message = message;
|
|
17650
|
-
run.error_detail = source ?
|
|
18221
|
+
run.error_detail = source ? readErrorMessage3(source.payload) ?? void 0 : void 0;
|
|
17651
18222
|
const visibleMessage = formatFailureMessage(message, run.error_detail);
|
|
17652
18223
|
const usage = readUsage(source?.payload);
|
|
17653
18224
|
if (usage) {
|
|
@@ -17685,12 +18256,56 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17685
18256
|
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
17686
18257
|
});
|
|
17687
18258
|
await this.deps.persistConversationStats(conversationId, snapshot);
|
|
18259
|
+
void this.reportRunNotification({
|
|
18260
|
+
conversationId,
|
|
18261
|
+
run,
|
|
18262
|
+
assistant,
|
|
18263
|
+
eventKind: "run_failed",
|
|
18264
|
+
occurredAt: run.completed_at,
|
|
18265
|
+
fallbackPreview: visibleMessage
|
|
18266
|
+
});
|
|
17688
18267
|
void this.deps.logger.warn("conversation_run_failed", {
|
|
17689
18268
|
conversation_id: conversationId,
|
|
17690
18269
|
run_id: runId,
|
|
17691
18270
|
error: message
|
|
17692
18271
|
});
|
|
17693
18272
|
}
|
|
18273
|
+
async reportRunNotification(input) {
|
|
18274
|
+
const manifest = await this.deps.readActiveManifest(input.conversationId).catch(() => null);
|
|
18275
|
+
if (!manifest) {
|
|
18276
|
+
return;
|
|
18277
|
+
}
|
|
18278
|
+
const accountId = input.run.owner_account_id ?? manifest.owner_account_id;
|
|
18279
|
+
if (!accountId) {
|
|
18280
|
+
return;
|
|
18281
|
+
}
|
|
18282
|
+
await reportNotificationEventToServer({
|
|
18283
|
+
paths: this.deps.paths,
|
|
18284
|
+
logger: this.deps.logger,
|
|
18285
|
+
event: {
|
|
18286
|
+
sourceEventId: runNotificationSourceEventId(
|
|
18287
|
+
input.conversationId,
|
|
18288
|
+
input.run.id,
|
|
18289
|
+
input.eventKind
|
|
18290
|
+
),
|
|
18291
|
+
eventKind: input.eventKind,
|
|
18292
|
+
conversationId: input.conversationId,
|
|
18293
|
+
conversationTitle: manifest.title,
|
|
18294
|
+
accountId,
|
|
18295
|
+
messageId: input.assistant?.id ?? input.run.assistant_message_id,
|
|
18296
|
+
runId: input.run.id,
|
|
18297
|
+
bodyPreview: previewText2(input.assistant) ?? input.fallbackPreview,
|
|
18298
|
+
occurredAt: input.occurredAt
|
|
18299
|
+
}
|
|
18300
|
+
}).catch((error) => {
|
|
18301
|
+
void this.deps.logger.warn("notification_event_report_failed", {
|
|
18302
|
+
conversation_id: input.conversationId,
|
|
18303
|
+
run_id: input.run.id,
|
|
18304
|
+
event_kind: input.eventKind,
|
|
18305
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18306
|
+
});
|
|
18307
|
+
});
|
|
18308
|
+
}
|
|
17694
18309
|
async cancelRunLocked(conversationId, runId, options) {
|
|
17695
18310
|
const manifest = await this.deps.readRunnableManifest(conversationId);
|
|
17696
18311
|
const snapshot = await this.deps.readSnapshot(conversationId);
|
|
@@ -17721,6 +18336,13 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17721
18336
|
});
|
|
17722
18337
|
}
|
|
17723
18338
|
}
|
|
18339
|
+
if (!run.hermes_response_id) {
|
|
18340
|
+
void this.deps.logger.warn("interrupted_response_id_missing", {
|
|
18341
|
+
conversation_id: conversationId,
|
|
18342
|
+
run_id: runId,
|
|
18343
|
+
reason: options.reason
|
|
18344
|
+
});
|
|
18345
|
+
}
|
|
17724
18346
|
const cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17725
18347
|
run.status = "cancelled";
|
|
17726
18348
|
run.completed_at = cancelledAt;
|
|
@@ -17806,7 +18428,9 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17806
18428
|
profileName,
|
|
17807
18429
|
conversationId: input.conversationId,
|
|
17808
18430
|
beforeJobIds: input.beforeJobIds,
|
|
17809
|
-
jobs
|
|
18431
|
+
jobs,
|
|
18432
|
+
ownerAccountId: input.ownerAccountId,
|
|
18433
|
+
ownerAppInstanceId: input.ownerAppInstanceId
|
|
17810
18434
|
});
|
|
17811
18435
|
}
|
|
17812
18436
|
};
|
|
@@ -17824,7 +18448,19 @@ function buildRunInstructions(run, deliveryStagingDir) {
|
|
|
17824
18448
|
"Current runtime selected by Hermes Link:",
|
|
17825
18449
|
`- Model: ${run.model ?? "hermes-agent"}`,
|
|
17826
18450
|
`- Provider: ${run.provider ?? "unknown"}`,
|
|
17827
|
-
"If the user asks what model or provider you are currently using, answer from this runtime selection instead of inferring from earlier conversation history."
|
|
18451
|
+
"If the user asks what model or provider you are currently using, answer from this runtime selection instead of inferring from earlier conversation history.",
|
|
18452
|
+
...run.queue_mode === "guided_interrupt" ? ["", "Guided interrupt handling:", guidedInterruptNote()] : []
|
|
18453
|
+
].join("\n");
|
|
18454
|
+
}
|
|
18455
|
+
function guidedInterruptInputPrefix(run) {
|
|
18456
|
+
return run.queue_mode === "guided_interrupt" ? guidedInterruptNote() : "";
|
|
18457
|
+
}
|
|
18458
|
+
function guidedInterruptNote() {
|
|
18459
|
+
return [
|
|
18460
|
+
"System note from Hermes Link: The previous assistant turn was interrupted by the user.",
|
|
18461
|
+
"Treat the following user message as the controlling instruction for this turn.",
|
|
18462
|
+
"Do not continue, retry, or complete the interrupted task or its tool calls unless the following user message explicitly asks you to do so.",
|
|
18463
|
+
"Any partial tool results from the interrupted turn are context only, not an instruction to keep working on that task."
|
|
17828
18464
|
].join("\n");
|
|
17829
18465
|
}
|
|
17830
18466
|
function appendMediaImportFailureNotice(message) {
|
|
@@ -18088,7 +18724,9 @@ function findPreviousHermesResponseId(snapshot, run) {
|
|
|
18088
18724
|
if (!currentProfile) {
|
|
18089
18725
|
return void 0;
|
|
18090
18726
|
}
|
|
18091
|
-
const candidates = snapshot.runs.filter((item) => item.id !== run.id).filter((item) => item.kind !== "command").filter(
|
|
18727
|
+
const candidates = snapshot.runs.filter((item) => item.id !== run.id).filter((item) => item.kind !== "command").filter(
|
|
18728
|
+
(item) => item.status === "completed" || item.status === "cancelled"
|
|
18729
|
+
).filter((item) => item.hermes_response_id).filter((item) => item.hermes_session_id === run.hermes_session_id).filter(
|
|
18092
18730
|
(item) => normalizeRunProfileForCompare(
|
|
18093
18731
|
item.profile_name_snapshot ?? item.profile
|
|
18094
18732
|
) === currentProfile
|
|
@@ -18171,6 +18809,17 @@ function readStatusErrorMessage(value) {
|
|
|
18171
18809
|
function formatUnknownErrorMessage(error) {
|
|
18172
18810
|
return error instanceof Error ? error.message : String(error);
|
|
18173
18811
|
}
|
|
18812
|
+
function previewText2(message) {
|
|
18813
|
+
if (!message) {
|
|
18814
|
+
return null;
|
|
18815
|
+
}
|
|
18816
|
+
const text = messageText(message).replace(/<[^>]+>/gu, " ").replace(/\s+/gu, " ").trim();
|
|
18817
|
+
return text ? text.slice(0, 512) : null;
|
|
18818
|
+
}
|
|
18819
|
+
function runNotificationSourceEventId(conversationId, runId, eventKind) {
|
|
18820
|
+
const digest = createHash5("sha256").update(`${conversationId}:${runId}:${eventKind}`).digest("hex").slice(0, 24);
|
|
18821
|
+
return `${conversationId}:${eventKind}:${digest}`;
|
|
18822
|
+
}
|
|
18174
18823
|
async function closeSseIterator(iterator) {
|
|
18175
18824
|
if (!iterator.return) {
|
|
18176
18825
|
return;
|
|
@@ -18303,6 +18952,7 @@ var ConversationService = class {
|
|
|
18303
18952
|
queries;
|
|
18304
18953
|
runLifecycle;
|
|
18305
18954
|
hermesSessionSyncPromise = null;
|
|
18955
|
+
cronDeliverySyncPromise = null;
|
|
18306
18956
|
async withConversationLock(conversationId, task) {
|
|
18307
18957
|
const previous = this.conversationLocks.get(conversationId) ?? Promise.resolve();
|
|
18308
18958
|
let release;
|
|
@@ -18413,6 +19063,8 @@ var ConversationService = class {
|
|
|
18413
19063
|
profile_uid: profile.profileUid,
|
|
18414
19064
|
profile_name_snapshot: profile.profileName,
|
|
18415
19065
|
profile: profile.profileName,
|
|
19066
|
+
owner_account_id: input.accountId,
|
|
19067
|
+
owner_app_instance_id: input.appInstanceId,
|
|
18416
19068
|
created_at: now,
|
|
18417
19069
|
updated_at: now,
|
|
18418
19070
|
last_event_seq: 0
|
|
@@ -18466,19 +19118,115 @@ var ConversationService = class {
|
|
|
18466
19118
|
return created.id;
|
|
18467
19119
|
}
|
|
18468
19120
|
async appendCronDelivery(input) {
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
19121
|
+
if (input.source === "natural_language" && input.conversationId) {
|
|
19122
|
+
return this.appendCronDeliveryToBoundConversation(input);
|
|
19123
|
+
}
|
|
19124
|
+
await syncHermesCronSessionIntoConversations(
|
|
19125
|
+
this.paths,
|
|
19126
|
+
this.logger,
|
|
19127
|
+
{
|
|
19128
|
+
profileName: input.profileName,
|
|
19129
|
+
jobId: input.jobId,
|
|
19130
|
+
runAt: input.runAt
|
|
19131
|
+
}
|
|
19132
|
+
).catch((error) => {
|
|
19133
|
+
void this.logger.warn("cron_notification_session_sync_failed", {
|
|
19134
|
+
job_id: input.jobId,
|
|
19135
|
+
output_path: input.outputPath,
|
|
19136
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19137
|
+
});
|
|
19138
|
+
return null;
|
|
19139
|
+
});
|
|
19140
|
+
const target = await this.findImportedCronConversation({
|
|
19141
|
+
profileName: input.profileName,
|
|
19142
|
+
jobId: input.jobId,
|
|
19143
|
+
runAt: input.runAt
|
|
19144
|
+
});
|
|
19145
|
+
if (!target) {
|
|
19146
|
+
void this.logger.info("cron_notification_skipped_no_conversation", {
|
|
19147
|
+
profile: input.profileName,
|
|
19148
|
+
job_id: input.jobId,
|
|
19149
|
+
output_path: input.outputPath
|
|
19150
|
+
});
|
|
19151
|
+
return false;
|
|
19152
|
+
}
|
|
19153
|
+
return this.withConversationLock(target.manifest.id, async () => {
|
|
19154
|
+
const manifest = await this.store.readActiveManifest(target.manifest.id);
|
|
19155
|
+
const snapshot = await this.store.readSnapshot(target.manifest.id);
|
|
19156
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19157
|
+
const ownerAccountId = input.accountId ?? manifest.owner_account_id;
|
|
19158
|
+
const ownerAppInstanceId = input.appInstanceId ?? manifest.owner_app_instance_id;
|
|
19159
|
+
const nextManifest = ownerAccountId && ownerAccountId !== manifest.owner_account_id ? {
|
|
19160
|
+
...manifest,
|
|
19161
|
+
owner_account_id: ownerAccountId,
|
|
19162
|
+
owner_app_instance_id: ownerAppInstanceId,
|
|
19163
|
+
updated_at: now
|
|
19164
|
+
} : manifest;
|
|
19165
|
+
if (nextManifest !== manifest) {
|
|
19166
|
+
await this.store.writeManifest(nextManifest);
|
|
19167
|
+
}
|
|
19168
|
+
const message = selectCronNotificationMessage(snapshot, input.runAt);
|
|
19169
|
+
return this.reportCronNotification({
|
|
19170
|
+
manifest: nextManifest,
|
|
19171
|
+
message,
|
|
19172
|
+
jobId: input.jobId,
|
|
19173
|
+
outputPath: input.outputPath,
|
|
19174
|
+
failed: input.failed === true,
|
|
19175
|
+
occurredAt: input.runAt ?? now,
|
|
19176
|
+
accountId: ownerAccountId,
|
|
19177
|
+
appInstanceId: ownerAppInstanceId,
|
|
19178
|
+
bodyPreview: message != null ? notificationPreviewText(message) : input.content
|
|
19179
|
+
});
|
|
19180
|
+
});
|
|
19181
|
+
}
|
|
19182
|
+
async appendCronDeliveryToBoundConversation(input) {
|
|
19183
|
+
const conversationId = input.conversationId?.trim();
|
|
19184
|
+
if (!conversationId) {
|
|
19185
|
+
return true;
|
|
19186
|
+
}
|
|
19187
|
+
return this.withConversationLock(conversationId, async () => {
|
|
19188
|
+
const manifest = await this.store.readActiveManifest(conversationId);
|
|
19189
|
+
const snapshot = await this.store.readSnapshot(conversationId);
|
|
19190
|
+
const existingMessage = snapshot.messages.find(
|
|
18473
19191
|
(message2) => message2.hermes?.cron_output_path === input.outputPath
|
|
18474
|
-
)
|
|
18475
|
-
|
|
19192
|
+
);
|
|
19193
|
+
if (existingMessage) {
|
|
19194
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
19195
|
+
const ownerAccountId2 = input.accountId ?? manifest.owner_account_id;
|
|
19196
|
+
const ownerAppInstanceId2 = input.appInstanceId ?? manifest.owner_app_instance_id;
|
|
19197
|
+
const nextManifest2 = ownerAccountId2 && ownerAccountId2 !== manifest.owner_account_id ? {
|
|
19198
|
+
...manifest,
|
|
19199
|
+
owner_account_id: ownerAccountId2,
|
|
19200
|
+
owner_app_instance_id: ownerAppInstanceId2,
|
|
19201
|
+
updated_at: now2
|
|
19202
|
+
} : manifest;
|
|
19203
|
+
if (nextManifest2 !== manifest) {
|
|
19204
|
+
await this.store.writeManifest(nextManifest2);
|
|
19205
|
+
}
|
|
19206
|
+
return this.reportCronNotification({
|
|
19207
|
+
manifest: nextManifest2,
|
|
19208
|
+
message: existingMessage,
|
|
19209
|
+
jobId: input.jobId,
|
|
19210
|
+
outputPath: input.outputPath,
|
|
19211
|
+
failed: input.failed === true,
|
|
19212
|
+
occurredAt: input.runAt ?? existingMessage.created_at,
|
|
19213
|
+
accountId: ownerAccountId2,
|
|
19214
|
+
appInstanceId: ownerAppInstanceId2
|
|
19215
|
+
});
|
|
18476
19216
|
}
|
|
18477
19217
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18478
19218
|
const createdAt = input.runAt ?? now;
|
|
18479
19219
|
const profileName = normalizeProfileName2(
|
|
18480
19220
|
manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
|
|
18481
19221
|
);
|
|
19222
|
+
const ownerAccountId = input.accountId ?? manifest.owner_account_id;
|
|
19223
|
+
const ownerAppInstanceId = input.appInstanceId ?? manifest.owner_app_instance_id;
|
|
19224
|
+
const nextManifest = ownerAccountId && ownerAccountId !== manifest.owner_account_id ? {
|
|
19225
|
+
...manifest,
|
|
19226
|
+
owner_account_id: ownerAccountId,
|
|
19227
|
+
owner_app_instance_id: ownerAppInstanceId,
|
|
19228
|
+
updated_at: now
|
|
19229
|
+
} : manifest;
|
|
18482
19230
|
const message = {
|
|
18483
19231
|
id: `msg_${randomUUID10().replaceAll("-", "")}`,
|
|
18484
19232
|
schema_version: 1,
|
|
@@ -18510,19 +19258,154 @@ var ConversationService = class {
|
|
|
18510
19258
|
output_path: input.outputPath
|
|
18511
19259
|
}
|
|
18512
19260
|
}
|
|
18513
|
-
};
|
|
18514
|
-
snapshot.messages.push(message);
|
|
18515
|
-
await this.store.writeSnapshot(
|
|
18516
|
-
|
|
18517
|
-
|
|
18518
|
-
|
|
18519
|
-
|
|
19261
|
+
};
|
|
19262
|
+
snapshot.messages.push(message);
|
|
19263
|
+
await this.store.writeSnapshot(nextManifest.id, snapshot);
|
|
19264
|
+
if (nextManifest !== manifest) {
|
|
19265
|
+
await this.store.writeManifest(nextManifest);
|
|
19266
|
+
}
|
|
19267
|
+
await this.appendEvent(nextManifest.id, {
|
|
19268
|
+
type: "message.created",
|
|
19269
|
+
message_id: message.id,
|
|
19270
|
+
payload: { message }
|
|
19271
|
+
});
|
|
19272
|
+
await this.persistConversationStats(nextManifest.id, snapshot);
|
|
19273
|
+
return this.reportCronNotification({
|
|
19274
|
+
manifest: nextManifest,
|
|
19275
|
+
message,
|
|
19276
|
+
jobId: input.jobId,
|
|
19277
|
+
outputPath: input.outputPath,
|
|
19278
|
+
failed: input.failed === true,
|
|
19279
|
+
occurredAt: createdAt,
|
|
19280
|
+
accountId: ownerAccountId,
|
|
19281
|
+
appInstanceId: ownerAppInstanceId
|
|
19282
|
+
});
|
|
19283
|
+
});
|
|
19284
|
+
}
|
|
19285
|
+
async findImportedCronConversation(input) {
|
|
19286
|
+
const profileName = normalizeProfileName2(input.profileName);
|
|
19287
|
+
const outputAt = Date.parse(input.runAt ?? "");
|
|
19288
|
+
const candidates = [];
|
|
19289
|
+
for (const conversationId of await this.store.listConversationIds()) {
|
|
19290
|
+
const manifest = await this.store.readManifest(conversationId).catch(() => null);
|
|
19291
|
+
if (!manifest || manifest.status !== "active") {
|
|
19292
|
+
continue;
|
|
19293
|
+
}
|
|
19294
|
+
if (normalizeProfileName2(manifest.profile_name_snapshot ?? manifest.profile) !== profileName) {
|
|
19295
|
+
continue;
|
|
19296
|
+
}
|
|
19297
|
+
const sessionIds = conversationHermesSessionIds(manifest);
|
|
19298
|
+
const cronSessionId = sessionIds.find(
|
|
19299
|
+
(sessionId) => isCronSessionIdForJob(sessionId, input.jobId)
|
|
19300
|
+
);
|
|
19301
|
+
if (!cronSessionId) {
|
|
19302
|
+
continue;
|
|
19303
|
+
}
|
|
19304
|
+
candidates.push({
|
|
19305
|
+
manifest,
|
|
19306
|
+
sessionStartedAt: cronSessionStartedAt(cronSessionId, input.jobId),
|
|
19307
|
+
updatedAt: Date.parse(manifest.updated_at)
|
|
19308
|
+
});
|
|
19309
|
+
}
|
|
19310
|
+
if (candidates.length === 0) {
|
|
19311
|
+
return null;
|
|
19312
|
+
}
|
|
19313
|
+
candidates.sort((left, right) => {
|
|
19314
|
+
const leftTime = Number.isNaN(left.sessionStartedAt) ? left.updatedAt : left.sessionStartedAt;
|
|
19315
|
+
const rightTime = Number.isNaN(right.sessionStartedAt) ? right.updatedAt : right.sessionStartedAt;
|
|
19316
|
+
if (!Number.isNaN(outputAt)) {
|
|
19317
|
+
const leftFuture = leftTime > outputAt + 1e4 ? 1 : 0;
|
|
19318
|
+
const rightFuture = rightTime > outputAt + 1e4 ? 1 : 0;
|
|
19319
|
+
if (leftFuture !== rightFuture) {
|
|
19320
|
+
return leftFuture - rightFuture;
|
|
19321
|
+
}
|
|
19322
|
+
}
|
|
19323
|
+
return rightTime - leftTime;
|
|
19324
|
+
});
|
|
19325
|
+
return { manifest: candidates[0].manifest };
|
|
19326
|
+
}
|
|
19327
|
+
async reportCronNotification(input) {
|
|
19328
|
+
const accountId = input.accountId ?? input.manifest.owner_account_id;
|
|
19329
|
+
if (!accountId) {
|
|
19330
|
+
return false;
|
|
19331
|
+
}
|
|
19332
|
+
try {
|
|
19333
|
+
await reportNotificationEventToServer({
|
|
19334
|
+
paths: this.paths,
|
|
19335
|
+
logger: this.logger,
|
|
19336
|
+
event: {
|
|
19337
|
+
sourceEventId: cronNotificationSourceEventId(
|
|
19338
|
+
input.manifest.id,
|
|
19339
|
+
input.jobId,
|
|
19340
|
+
input.outputPath,
|
|
19341
|
+
input.failed ? "cron_failed" : "cron_completed"
|
|
19342
|
+
),
|
|
19343
|
+
eventKind: input.failed ? "cron_failed" : "cron_completed",
|
|
19344
|
+
conversationId: input.manifest.id,
|
|
19345
|
+
conversationTitle: input.manifest.title,
|
|
19346
|
+
accountId,
|
|
19347
|
+
messageId: input.message?.id,
|
|
19348
|
+
bodyPreview: input.bodyPreview ?? (input.message ? notificationPreviewText(input.message) : null),
|
|
19349
|
+
occurredAt: input.occurredAt
|
|
19350
|
+
}
|
|
18520
19351
|
});
|
|
18521
|
-
|
|
18522
|
-
})
|
|
19352
|
+
return true;
|
|
19353
|
+
} catch (error) {
|
|
19354
|
+
void this.logger.warn("notification_event_report_failed", {
|
|
19355
|
+
conversation_id: input.manifest.id,
|
|
19356
|
+
job_id: input.jobId,
|
|
19357
|
+
event_kind: input.failed ? "cron_failed" : "cron_completed",
|
|
19358
|
+
error: error instanceof Error ? error.message : String(error)
|
|
19359
|
+
});
|
|
19360
|
+
return false;
|
|
19361
|
+
}
|
|
18523
19362
|
}
|
|
18524
19363
|
async syncCronDeliveries() {
|
|
18525
|
-
|
|
19364
|
+
if (this.cronDeliverySyncPromise) {
|
|
19365
|
+
return this.cronDeliverySyncPromise;
|
|
19366
|
+
}
|
|
19367
|
+
const task = syncHermesLinkCronDeliveries(this.paths, this, this.logger);
|
|
19368
|
+
this.cronDeliverySyncPromise = task;
|
|
19369
|
+
try {
|
|
19370
|
+
await task;
|
|
19371
|
+
} finally {
|
|
19372
|
+
if (this.cronDeliverySyncPromise === task) {
|
|
19373
|
+
this.cronDeliverySyncPromise = null;
|
|
19374
|
+
}
|
|
19375
|
+
}
|
|
19376
|
+
}
|
|
19377
|
+
async backfillCronOwnership(input) {
|
|
19378
|
+
const accountId = input.accountId?.trim();
|
|
19379
|
+
if (!accountId) {
|
|
19380
|
+
return;
|
|
19381
|
+
}
|
|
19382
|
+
const bindingCount = await backfillHermesLinkCronDeliveryOwner(this.paths, {
|
|
19383
|
+
accountId,
|
|
19384
|
+
appInstanceId: input.appInstanceId
|
|
19385
|
+
});
|
|
19386
|
+
let conversationCount = 0;
|
|
19387
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19388
|
+
for (const conversationId of await this.store.listConversationIds()) {
|
|
19389
|
+
const manifest = await this.store.readManifest(conversationId).catch(() => null);
|
|
19390
|
+
if (!manifest || manifest.status === "deleted_soft" || manifest.owner_account_id || !manifestHasCronSession(manifest)) {
|
|
19391
|
+
continue;
|
|
19392
|
+
}
|
|
19393
|
+
await this.store.writeManifest({
|
|
19394
|
+
...manifest,
|
|
19395
|
+
owner_account_id: accountId,
|
|
19396
|
+
owner_app_instance_id: input.appInstanceId,
|
|
19397
|
+
updated_at: now
|
|
19398
|
+
});
|
|
19399
|
+
conversationCount += 1;
|
|
19400
|
+
}
|
|
19401
|
+
if (bindingCount > 0 || conversationCount > 0) {
|
|
19402
|
+
void this.logger.info("cron_owner_backfilled", {
|
|
19403
|
+
account_id: accountId,
|
|
19404
|
+
app_instance_id: input.appInstanceId ?? null,
|
|
19405
|
+
bindings: bindingCount,
|
|
19406
|
+
conversations: conversationCount
|
|
19407
|
+
});
|
|
19408
|
+
}
|
|
18526
19409
|
}
|
|
18527
19410
|
async syncHermesSessions() {
|
|
18528
19411
|
if (this.hermesSessionSyncPromise) {
|
|
@@ -18819,6 +19702,12 @@ var ConversationService = class {
|
|
|
18819
19702
|
}
|
|
18820
19703
|
return this.cancelRun(conversationId, runId);
|
|
18821
19704
|
}
|
|
19705
|
+
async guideQueuedRun(conversationId, runId) {
|
|
19706
|
+
return this.orchestration.guideQueuedRun(conversationId, runId);
|
|
19707
|
+
}
|
|
19708
|
+
async cancelQueuedRun(conversationId, runId) {
|
|
19709
|
+
return this.orchestration.cancelQueuedRun(conversationId, runId);
|
|
19710
|
+
}
|
|
18822
19711
|
async resolveApproval(input) {
|
|
18823
19712
|
const decision = input.decision;
|
|
18824
19713
|
if (decision === "once" || decision === "session") {
|
|
@@ -19130,9 +20019,76 @@ function findApproval(snapshot, approvalId) {
|
|
|
19130
20019
|
}
|
|
19131
20020
|
return null;
|
|
19132
20021
|
}
|
|
20022
|
+
function cronNotificationSourceEventId(conversationId, jobId, outputPath, eventKind) {
|
|
20023
|
+
const digest = createHash6("sha256").update(`${conversationId}:${jobId}:${outputPath}:${eventKind}`).digest("hex").slice(0, 24);
|
|
20024
|
+
return `${conversationId}:${eventKind}:${digest}`;
|
|
20025
|
+
}
|
|
20026
|
+
function conversationHermesSessionIds(manifest) {
|
|
20027
|
+
const ids = [
|
|
20028
|
+
manifest.hermes_session_id,
|
|
20029
|
+
...manifest.hermes_session_ids ?? [],
|
|
20030
|
+
manifest.hermes_lineage?.root_session_id,
|
|
20031
|
+
manifest.hermes_lineage?.current_session_id,
|
|
20032
|
+
...manifest.hermes_lineage?.session_ids ?? []
|
|
20033
|
+
].map((id) => id?.trim()).filter((id) => Boolean(id));
|
|
20034
|
+
return [...new Set(ids)];
|
|
20035
|
+
}
|
|
20036
|
+
function manifestHasCronSession(manifest) {
|
|
20037
|
+
return conversationHermesSessionIds(manifest).some(
|
|
20038
|
+
(sessionId) => sessionId.startsWith("cron_")
|
|
20039
|
+
);
|
|
20040
|
+
}
|
|
20041
|
+
function isCronSessionIdForJob(sessionId, jobId) {
|
|
20042
|
+
const normalizedJobId = jobId.trim();
|
|
20043
|
+
return normalizedJobId.length > 0 && sessionId.startsWith(`cron_${normalizedJobId}_`);
|
|
20044
|
+
}
|
|
20045
|
+
function cronSessionStartedAt(sessionId, jobId) {
|
|
20046
|
+
const normalizedJobId = jobId.trim();
|
|
20047
|
+
const prefix = `cron_${normalizedJobId}_`;
|
|
20048
|
+
if (!sessionId.startsWith(prefix)) {
|
|
20049
|
+
return Number.NaN;
|
|
20050
|
+
}
|
|
20051
|
+
const stamp = sessionId.slice(prefix.length);
|
|
20052
|
+
const match = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/u.exec(stamp);
|
|
20053
|
+
if (!match) {
|
|
20054
|
+
return Number.NaN;
|
|
20055
|
+
}
|
|
20056
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
20057
|
+
return new Date(
|
|
20058
|
+
Number(year),
|
|
20059
|
+
Number(month) - 1,
|
|
20060
|
+
Number(day),
|
|
20061
|
+
Number(hour),
|
|
20062
|
+
Number(minute),
|
|
20063
|
+
Number(second)
|
|
20064
|
+
).getTime();
|
|
20065
|
+
}
|
|
20066
|
+
function selectCronNotificationMessage(snapshot, runAt) {
|
|
20067
|
+
const messages2 = snapshot.messages.filter(
|
|
20068
|
+
(message) => message.role === "assistant"
|
|
20069
|
+
);
|
|
20070
|
+
if (messages2.length === 0) {
|
|
20071
|
+
return null;
|
|
20072
|
+
}
|
|
20073
|
+
const targetTime = Date.parse(runAt ?? "");
|
|
20074
|
+
if (Number.isNaN(targetTime)) {
|
|
20075
|
+
return messages2.at(-1) ?? null;
|
|
20076
|
+
}
|
|
20077
|
+
return [...messages2].sort((left, right) => {
|
|
20078
|
+
const leftTime = Date.parse(left.created_at);
|
|
20079
|
+
const rightTime = Date.parse(right.created_at);
|
|
20080
|
+
const leftDistance = Number.isNaN(leftTime) ? Number.POSITIVE_INFINITY : Math.abs(leftTime - targetTime);
|
|
20081
|
+
const rightDistance = Number.isNaN(rightTime) ? Number.POSITIVE_INFINITY : Math.abs(rightTime - targetTime);
|
|
20082
|
+
return leftDistance - rightDistance;
|
|
20083
|
+
})[0] ?? null;
|
|
20084
|
+
}
|
|
20085
|
+
function notificationPreviewText(message) {
|
|
20086
|
+
const text = messageText(message).replace(/<[^>]+>/gu, " ").replace(/\s+/gu, " ").trim();
|
|
20087
|
+
return text ? text.slice(0, 512) : null;
|
|
20088
|
+
}
|
|
19133
20089
|
|
|
19134
20090
|
// src/security/devices.ts
|
|
19135
|
-
import { randomBytes as randomBytes2, randomUUID as randomUUID11, timingSafeEqual, createHash as
|
|
20091
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID11, timingSafeEqual, createHash as createHash7 } from "crypto";
|
|
19136
20092
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
19137
20093
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
19138
20094
|
var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -19428,7 +20384,7 @@ function randomToken(prefix) {
|
|
|
19428
20384
|
return `${prefix}${randomBytes2(24).toString("base64url")}`;
|
|
19429
20385
|
}
|
|
19430
20386
|
function sha256(value) {
|
|
19431
|
-
return
|
|
20387
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
19432
20388
|
}
|
|
19433
20389
|
function safeEqual(left, right) {
|
|
19434
20390
|
const leftBytes = Buffer.from(left);
|
|
@@ -19508,7 +20464,8 @@ async function authenticateRequest(ctx, paths) {
|
|
|
19508
20464
|
}
|
|
19509
20465
|
const device = await authenticateDeviceAccessToken(token, paths);
|
|
19510
20466
|
if (device) {
|
|
19511
|
-
|
|
20467
|
+
const owner = await readOptionalAppConnectOwner(ctx, paths);
|
|
20468
|
+
return { kind: "device", device, ...owner };
|
|
19512
20469
|
}
|
|
19513
20470
|
if (token.startsWith("hpat_")) {
|
|
19514
20471
|
throw new LinkHttpError(
|
|
@@ -19532,6 +20489,31 @@ async function authenticateRequest(ctx, paths) {
|
|
|
19532
20489
|
appInstanceId: normalizeAppInstanceId2(claims.app_instance_id)
|
|
19533
20490
|
};
|
|
19534
20491
|
}
|
|
20492
|
+
async function readOptionalAppConnectOwner(ctx, paths) {
|
|
20493
|
+
const token = readOptionalHeaderToken(
|
|
20494
|
+
ctx.get("x-hermespilot-app-connect-token")
|
|
20495
|
+
);
|
|
20496
|
+
if (!token) {
|
|
20497
|
+
return {};
|
|
20498
|
+
}
|
|
20499
|
+
try {
|
|
20500
|
+
const [identity, config] = await Promise.all([
|
|
20501
|
+
loadRequiredIdentity(paths),
|
|
20502
|
+
loadConfig(paths)
|
|
20503
|
+
]);
|
|
20504
|
+
const claims = await verifyAppConnectToken(token, {
|
|
20505
|
+
config,
|
|
20506
|
+
linkId: identity.link_id
|
|
20507
|
+
});
|
|
20508
|
+
return {
|
|
20509
|
+
accountId: claims.sub,
|
|
20510
|
+
scopes: normalizeScopes(claims.scope),
|
|
20511
|
+
appInstanceId: normalizeAppInstanceId2(claims.app_instance_id)
|
|
20512
|
+
};
|
|
20513
|
+
} catch {
|
|
20514
|
+
return {};
|
|
20515
|
+
}
|
|
20516
|
+
}
|
|
19535
20517
|
async function loadRequiredIdentity(paths) {
|
|
19536
20518
|
const identity = await loadIdentity(paths);
|
|
19537
20519
|
if (!identity?.link_id) {
|
|
@@ -19547,6 +20529,16 @@ function readBearerToken(value) {
|
|
|
19547
20529
|
const token = trimmed.slice(7).trim();
|
|
19548
20530
|
return token || null;
|
|
19549
20531
|
}
|
|
20532
|
+
function readOptionalHeaderToken(value) {
|
|
20533
|
+
const trimmed = value.trim();
|
|
20534
|
+
if (!trimmed) {
|
|
20535
|
+
return null;
|
|
20536
|
+
}
|
|
20537
|
+
if (trimmed.toLowerCase().startsWith("bearer ")) {
|
|
20538
|
+
return readBearerToken(trimmed);
|
|
20539
|
+
}
|
|
20540
|
+
return trimmed;
|
|
20541
|
+
}
|
|
19550
20542
|
function normalizeScopes(value) {
|
|
19551
20543
|
if (Array.isArray(value)) {
|
|
19552
20544
|
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
@@ -19979,9 +20971,10 @@ function isExpectedClientDisconnectError(error) {
|
|
|
19979
20971
|
|
|
19980
20972
|
// src/http/routes/conversations.ts
|
|
19981
20973
|
function registerConversationRoutes(router, options) {
|
|
19982
|
-
const { paths, conversations } = options;
|
|
20974
|
+
const { paths, logger, conversations } = options;
|
|
19983
20975
|
router.get("/api/v1/conversations", async (ctx) => {
|
|
19984
|
-
await authenticateRequest(ctx, paths);
|
|
20976
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20977
|
+
await prepareConversationListRead(conversations, logger, auth);
|
|
19985
20978
|
ctx.set("cache-control", "no-store");
|
|
19986
20979
|
const result = await conversations.listConversationPage({
|
|
19987
20980
|
limit: readLimit(ctx.query.limit),
|
|
@@ -20008,7 +21001,8 @@ function registerConversationRoutes(router, options) {
|
|
|
20008
21001
|
};
|
|
20009
21002
|
});
|
|
20010
21003
|
router.get("/api/v1/conversations/archived", async (ctx) => {
|
|
20011
|
-
await authenticateRequest(ctx, paths);
|
|
21004
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21005
|
+
await prepareConversationListRead(conversations, logger, auth);
|
|
20012
21006
|
ctx.set("cache-control", "no-store");
|
|
20013
21007
|
const result = await conversations.listArchivedConversationPage({
|
|
20014
21008
|
limit: readLimit(ctx.query.limit),
|
|
@@ -20035,14 +21029,16 @@ function registerConversationRoutes(router, options) {
|
|
|
20035
21029
|
};
|
|
20036
21030
|
});
|
|
20037
21031
|
router.post("/api/v1/conversations", async (ctx) => {
|
|
20038
|
-
await authenticateRequest(ctx, paths);
|
|
21032
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20039
21033
|
const body = await readJsonBody(ctx.req);
|
|
20040
21034
|
ctx.status = 201;
|
|
20041
21035
|
ctx.body = {
|
|
20042
21036
|
ok: true,
|
|
20043
21037
|
conversation: await conversations.createConversation({
|
|
20044
21038
|
title: readString16(body, "title") ?? void 0,
|
|
20045
|
-
profileName: readOptionalProfileName(body)
|
|
21039
|
+
profileName: readOptionalProfileName(body),
|
|
21040
|
+
accountId: auth.accountId,
|
|
21041
|
+
appInstanceId: auth.appInstanceId
|
|
20046
21042
|
})
|
|
20047
21043
|
};
|
|
20048
21044
|
});
|
|
@@ -20111,7 +21107,7 @@ function registerConversationRoutes(router, options) {
|
|
|
20111
21107
|
);
|
|
20112
21108
|
});
|
|
20113
21109
|
router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
|
|
20114
|
-
await authenticateRequest(ctx, paths);
|
|
21110
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20115
21111
|
const body = await readJsonBody(ctx.req);
|
|
20116
21112
|
const content = readString16(body, "content") ?? readString16(body, "text") ?? readString16(body, "input") ?? "";
|
|
20117
21113
|
const attachments = readMessageAttachments(body.attachments ?? body.blobs);
|
|
@@ -20131,7 +21127,9 @@ function registerConversationRoutes(router, options) {
|
|
|
20131
21127
|
attachments,
|
|
20132
21128
|
clientMessageId: readString16(body, "client_message_id") ?? readString16(body, "clientMessageId") ?? void 0,
|
|
20133
21129
|
idempotencyKey: readHeader(ctx, "idempotency-key") ?? void 0,
|
|
20134
|
-
profileName: readOptionalProfileName(body)
|
|
21130
|
+
profileName: readOptionalProfileName(body),
|
|
21131
|
+
accountId: auth.accountId,
|
|
21132
|
+
appInstanceId: auth.appInstanceId
|
|
20135
21133
|
})
|
|
20136
21134
|
};
|
|
20137
21135
|
});
|
|
@@ -20185,12 +21183,20 @@ function registerConversationRoutes(router, options) {
|
|
|
20185
21183
|
ctx.body = { ok: true };
|
|
20186
21184
|
});
|
|
20187
21185
|
router.post("/api/v1/conversations/clear-plans", async (ctx) => {
|
|
20188
|
-
await authenticateRequest(ctx, paths);
|
|
21186
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20189
21187
|
const body = await readJsonBody(ctx.req);
|
|
20190
21188
|
const targetStatus = readConversationClearPlanTargetStatus(body);
|
|
20191
21189
|
const plan = await conversations.prepareClearAllConversationPlan(
|
|
20192
21190
|
targetStatus
|
|
20193
21191
|
);
|
|
21192
|
+
void logger.warn(
|
|
21193
|
+
"conversation_clear_plan_prepared",
|
|
21194
|
+
conversationMutationAuditFields(ctx, auth, {
|
|
21195
|
+
plan_id: plan.id,
|
|
21196
|
+
target_status: plan.target_status,
|
|
21197
|
+
total_count: plan.total_count
|
|
21198
|
+
})
|
|
21199
|
+
);
|
|
20194
21200
|
ctx.status = 201;
|
|
20195
21201
|
ctx.body = {
|
|
20196
21202
|
ok: true,
|
|
@@ -20208,10 +21214,19 @@ function registerConversationRoutes(router, options) {
|
|
|
20208
21214
|
router.post(
|
|
20209
21215
|
"/api/v1/conversations/clear-plans/:planId/execute",
|
|
20210
21216
|
async (ctx) => {
|
|
20211
|
-
await authenticateRequest(ctx, paths);
|
|
21217
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20212
21218
|
const plan = await conversations.startClearAllConversationPlan(
|
|
20213
21219
|
ctx.params.planId
|
|
20214
21220
|
);
|
|
21221
|
+
void logger.warn(
|
|
21222
|
+
"conversation_clear_plan_execute_requested",
|
|
21223
|
+
conversationMutationAuditFields(ctx, auth, {
|
|
21224
|
+
plan_id: plan.id,
|
|
21225
|
+
target_status: plan.target_status,
|
|
21226
|
+
total_count: plan.total_count,
|
|
21227
|
+
status: plan.status
|
|
21228
|
+
})
|
|
21229
|
+
);
|
|
20215
21230
|
ctx.status = plan.status === "completed" ? 200 : 202;
|
|
20216
21231
|
ctx.body = {
|
|
20217
21232
|
ok: true,
|
|
@@ -20260,7 +21275,7 @@ function registerConversationRoutes(router, options) {
|
|
|
20260
21275
|
}
|
|
20261
21276
|
);
|
|
20262
21277
|
router.delete("/api/v1/conversations", async (ctx) => {
|
|
20263
|
-
await authenticateRequest(ctx, paths);
|
|
21278
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20264
21279
|
const body = await readJsonBody(ctx.req);
|
|
20265
21280
|
const conversationIds = readStringArray(
|
|
20266
21281
|
body,
|
|
@@ -20275,6 +21290,14 @@ function registerConversationRoutes(router, options) {
|
|
|
20275
21290
|
);
|
|
20276
21291
|
}
|
|
20277
21292
|
const deleted = await conversations.deleteConversations(conversationIds);
|
|
21293
|
+
void logger.warn(
|
|
21294
|
+
"conversation_bulk_delete_requested",
|
|
21295
|
+
conversationMutationAuditFields(ctx, auth, {
|
|
21296
|
+
requested_count: conversationIds.length,
|
|
21297
|
+
deleted_count: deleted.deleted_count,
|
|
21298
|
+
failed_count: deleted.failed_count
|
|
21299
|
+
})
|
|
21300
|
+
);
|
|
20278
21301
|
const ok = deleted.failed_count === 0;
|
|
20279
21302
|
ctx.status = ok ? 200 : 409;
|
|
20280
21303
|
ctx.body = {
|
|
@@ -20302,6 +21325,33 @@ function registerConversationRoutes(router, options) {
|
|
|
20302
21325
|
};
|
|
20303
21326
|
}
|
|
20304
21327
|
);
|
|
21328
|
+
router.post(
|
|
21329
|
+
"/api/v1/conversations/:conversationId/queued-runs/:runId/guide",
|
|
21330
|
+
async (ctx) => {
|
|
21331
|
+
await authenticateRequest(ctx, paths);
|
|
21332
|
+
ctx.status = 202;
|
|
21333
|
+
ctx.body = {
|
|
21334
|
+
ok: true,
|
|
21335
|
+
...await conversations.guideQueuedRun(
|
|
21336
|
+
ctx.params.conversationId,
|
|
21337
|
+
ctx.params.runId
|
|
21338
|
+
)
|
|
21339
|
+
};
|
|
21340
|
+
}
|
|
21341
|
+
);
|
|
21342
|
+
router.post(
|
|
21343
|
+
"/api/v1/conversations/:conversationId/queued-runs/:runId/cancel",
|
|
21344
|
+
async (ctx) => {
|
|
21345
|
+
await authenticateRequest(ctx, paths);
|
|
21346
|
+
ctx.body = {
|
|
21347
|
+
ok: true,
|
|
21348
|
+
...await conversations.cancelQueuedRun(
|
|
21349
|
+
ctx.params.conversationId,
|
|
21350
|
+
ctx.params.runId
|
|
21351
|
+
)
|
|
21352
|
+
};
|
|
21353
|
+
}
|
|
21354
|
+
);
|
|
20305
21355
|
router.post(
|
|
20306
21356
|
"/api/v1/conversations/:conversationId/approvals/:approvalId/approve",
|
|
20307
21357
|
async (ctx) => {
|
|
@@ -20354,10 +21404,21 @@ function registerConversationRoutes(router, options) {
|
|
|
20354
21404
|
}
|
|
20355
21405
|
);
|
|
20356
21406
|
router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
|
|
20357
|
-
await authenticateRequest(ctx, paths);
|
|
21407
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21408
|
+
const result = await conversations.deleteConversation(
|
|
21409
|
+
ctx.params.conversationId
|
|
21410
|
+
);
|
|
21411
|
+
void logger.warn(
|
|
21412
|
+
"conversation_delete_requested",
|
|
21413
|
+
conversationMutationAuditFields(ctx, auth, {
|
|
21414
|
+
conversation_id: result.conversation_id,
|
|
21415
|
+
hermes_deleted: result.hermes_deleted,
|
|
21416
|
+
hermes_session_count: result.hermes_session_ids?.length ?? 0
|
|
21417
|
+
})
|
|
21418
|
+
);
|
|
20358
21419
|
ctx.body = {
|
|
20359
21420
|
ok: true,
|
|
20360
|
-
...
|
|
21421
|
+
...result,
|
|
20361
21422
|
blob_gc_completed: true
|
|
20362
21423
|
};
|
|
20363
21424
|
});
|
|
@@ -20404,6 +21465,22 @@ function registerConversationRoutes(router, options) {
|
|
|
20404
21465
|
}
|
|
20405
21466
|
);
|
|
20406
21467
|
}
|
|
21468
|
+
async function prepareConversationListRead(conversations, logger, auth) {
|
|
21469
|
+
await conversations.backfillCronOwnership({
|
|
21470
|
+
accountId: auth.accountId,
|
|
21471
|
+
appInstanceId: auth.appInstanceId
|
|
21472
|
+
}).catch((error) => {
|
|
21473
|
+
void logger.warn("cron_owner_backfill_failed", {
|
|
21474
|
+
error: error instanceof Error ? error.message : String(error)
|
|
21475
|
+
});
|
|
21476
|
+
});
|
|
21477
|
+
await conversations.syncCronDeliveries().catch((error) => {
|
|
21478
|
+
void logger.warn("cron_link_delivery_sync_failed", {
|
|
21479
|
+
source: "conversation_list_read",
|
|
21480
|
+
error: error instanceof Error ? error.message : String(error)
|
|
21481
|
+
});
|
|
21482
|
+
});
|
|
21483
|
+
}
|
|
20407
21484
|
function resolveConversationEventCursor(input) {
|
|
20408
21485
|
const queryAfter = readInteger3(input.queryAfter) ?? 0;
|
|
20409
21486
|
const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
|
|
@@ -20420,6 +21497,21 @@ function readConversationClearPlanTargetStatus(body) {
|
|
|
20420
21497
|
"Conversation clear plan target status is invalid"
|
|
20421
21498
|
);
|
|
20422
21499
|
}
|
|
21500
|
+
function conversationMutationAuditFields(ctx, auth, fields) {
|
|
21501
|
+
return {
|
|
21502
|
+
method: ctx.method,
|
|
21503
|
+
path: ctx.path,
|
|
21504
|
+
auth_kind: auth.kind,
|
|
21505
|
+
device_id: auth.device?.id ?? null,
|
|
21506
|
+
device_label: auth.device?.label ?? null,
|
|
21507
|
+
device_platform: auth.device?.platform ?? null,
|
|
21508
|
+
device_model: auth.device?.model ?? null,
|
|
21509
|
+
account_id: auth.accountId ?? null,
|
|
21510
|
+
app_instance_id: auth.appInstanceId ?? null,
|
|
21511
|
+
user_agent: ctx.get("user-agent") || null,
|
|
21512
|
+
...fields
|
|
21513
|
+
};
|
|
21514
|
+
}
|
|
20423
21515
|
function readNonNegativeIntegerHeader(value) {
|
|
20424
21516
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
20425
21517
|
if (!raw) {
|
|
@@ -20971,8 +22063,8 @@ function toRecord14(value) {
|
|
|
20971
22063
|
function registerCronJobRoutes(router, options) {
|
|
20972
22064
|
const { paths, logger, conversations, syncCronDeliveries } = options;
|
|
20973
22065
|
router.get("/api/v1/cron-jobs", async (ctx) => {
|
|
20974
|
-
await authenticateRequest(ctx, paths);
|
|
20975
|
-
await syncCronDeliveries
|
|
22066
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
22067
|
+
await prepareCronJobRead(conversations, logger, auth, syncCronDeliveries);
|
|
20976
22068
|
ctx.set("cache-control", "no-store");
|
|
20977
22069
|
const includeDisabled = readQueryString(ctx.query.include_disabled)?.toLowerCase() === "true" || readQueryString(ctx.query.includeDisabled)?.toLowerCase() === "true";
|
|
20978
22070
|
const profiles = await listHermesProfiles(paths);
|
|
@@ -21011,8 +22103,8 @@ function registerCronJobRoutes(router, options) {
|
|
|
21011
22103
|
ctx.body = { ok: failures.length === 0, jobs, failures };
|
|
21012
22104
|
});
|
|
21013
22105
|
router.get("/api/v1/profiles/:name/cron-jobs", async (ctx) => {
|
|
21014
|
-
await authenticateRequest(ctx, paths);
|
|
21015
|
-
await syncCronDeliveries
|
|
22106
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
22107
|
+
await prepareCronJobRead(conversations, logger, auth, syncCronDeliveries);
|
|
21016
22108
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21017
22109
|
ctx.set("cache-control", "no-store");
|
|
21018
22110
|
const includeDisabled = readQueryString(ctx.query.include_disabled)?.toLowerCase() === "true" || readQueryString(ctx.query.includeDisabled)?.toLowerCase() === "true";
|
|
@@ -21031,7 +22123,7 @@ function registerCronJobRoutes(router, options) {
|
|
|
21031
22123
|
};
|
|
21032
22124
|
});
|
|
21033
22125
|
router.post("/api/v1/profiles/:name/cron-jobs", async (ctx) => {
|
|
21034
|
-
await authenticateRequest(ctx, paths);
|
|
22126
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21035
22127
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21036
22128
|
const body = await readJsonBody(ctx.req);
|
|
21037
22129
|
const input = readCronJobCreateInput(body);
|
|
@@ -21045,7 +22137,9 @@ function registerCronJobRoutes(router, options) {
|
|
|
21045
22137
|
conversations,
|
|
21046
22138
|
profileName: profile.name,
|
|
21047
22139
|
job,
|
|
21048
|
-
source: "app"
|
|
22140
|
+
source: "app",
|
|
22141
|
+
accountId: auth.accountId,
|
|
22142
|
+
appInstanceId: auth.appInstanceId
|
|
21049
22143
|
}) : job;
|
|
21050
22144
|
ctx.status = 201;
|
|
21051
22145
|
ctx.body = { ok: true, job: attachCronJobProfile(decoratedJob, profile) };
|
|
@@ -21067,7 +22161,7 @@ function registerCronJobRoutes(router, options) {
|
|
|
21067
22161
|
};
|
|
21068
22162
|
});
|
|
21069
22163
|
router.patch("/api/v1/profiles/:name/cron-jobs/:jobId", async (ctx) => {
|
|
21070
|
-
await authenticateRequest(ctx, paths);
|
|
22164
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21071
22165
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21072
22166
|
const body = await readJsonBody(ctx.req);
|
|
21073
22167
|
const input = readCronJobUpdateInput(body);
|
|
@@ -21088,7 +22182,9 @@ function registerCronJobRoutes(router, options) {
|
|
|
21088
22182
|
conversations,
|
|
21089
22183
|
profileName: profile.name,
|
|
21090
22184
|
job,
|
|
21091
|
-
source: "app"
|
|
22185
|
+
source: "app",
|
|
22186
|
+
accountId: auth.accountId,
|
|
22187
|
+
appInstanceId: auth.appInstanceId
|
|
21092
22188
|
});
|
|
21093
22189
|
} else if (deliverTouched) {
|
|
21094
22190
|
await unbindCronJobFromHermesLink(paths, profile.name, ctx.params.jobId);
|
|
@@ -21153,6 +22249,18 @@ function registerCronJobRoutes(router, options) {
|
|
|
21153
22249
|
};
|
|
21154
22250
|
});
|
|
21155
22251
|
}
|
|
22252
|
+
async function prepareCronJobRead(conversations, logger, auth, syncCronDeliveries) {
|
|
22253
|
+
await conversations.backfillCronOwnership({
|
|
22254
|
+
accountId: auth.accountId,
|
|
22255
|
+
appInstanceId: auth.appInstanceId
|
|
22256
|
+
}).catch((error) => {
|
|
22257
|
+
void logger.warn("cron_owner_backfill_failed", {
|
|
22258
|
+
source: "cron_job_read",
|
|
22259
|
+
error: error instanceof Error ? error.message : String(error)
|
|
22260
|
+
});
|
|
22261
|
+
});
|
|
22262
|
+
await syncCronDeliveries();
|
|
22263
|
+
}
|
|
21156
22264
|
function toHermesCronJobInput(input) {
|
|
21157
22265
|
return {
|
|
21158
22266
|
...input,
|
|
@@ -21164,14 +22272,16 @@ async function bindAndDecorateCronJobForHermesLink(input) {
|
|
|
21164
22272
|
if (!jobId) {
|
|
21165
22273
|
return input.job;
|
|
21166
22274
|
}
|
|
21167
|
-
const conversationId = await input.conversations.ensureCronInboxConversation({
|
|
22275
|
+
const conversationId = input.source === "natural_language" ? await input.conversations.ensureCronInboxConversation({
|
|
21168
22276
|
profileName: input.profileName
|
|
21169
|
-
});
|
|
22277
|
+
}) : void 0;
|
|
21170
22278
|
await bindCronJobToHermesLink(input.paths, {
|
|
21171
22279
|
profileName: input.profileName,
|
|
21172
22280
|
jobId,
|
|
21173
22281
|
conversationId,
|
|
21174
|
-
source: input.source
|
|
22282
|
+
source: input.source,
|
|
22283
|
+
ownerAccountId: input.accountId,
|
|
22284
|
+
ownerAppInstanceId: input.appInstanceId
|
|
21175
22285
|
});
|
|
21176
22286
|
return { ...input.job, deliver: HERMES_LINK_CRON_DELIVER };
|
|
21177
22287
|
}
|
|
@@ -26263,12 +27373,10 @@ function computeRelayBackoffMs(attempt, options = {}) {
|
|
|
26263
27373
|
return exponential + Math.floor(exponential * ratio);
|
|
26264
27374
|
}
|
|
26265
27375
|
async function updateRelayReconnectState(paths, update) {
|
|
26266
|
-
|
|
26267
|
-
const next = {
|
|
27376
|
+
await updateJsonFile(paths.stateFile, (state) => ({
|
|
26268
27377
|
...state,
|
|
26269
|
-
relayReconnect: update(normalizeRelayReconnectState(state
|
|
26270
|
-
};
|
|
26271
|
-
await writeJsonFile(paths.stateFile, next);
|
|
27378
|
+
relayReconnect: update(normalizeRelayReconnectState(state?.relayReconnect))
|
|
27379
|
+
}));
|
|
26272
27380
|
}
|
|
26273
27381
|
async function readLinkState(paths) {
|
|
26274
27382
|
const state = await readJsonFile(paths.stateFile);
|
|
@@ -26375,6 +27483,9 @@ function readInteger4(value) {
|
|
|
26375
27483
|
}
|
|
26376
27484
|
|
|
26377
27485
|
// src/relay/control-client.ts
|
|
27486
|
+
var DEFAULT_RELAY_HANDSHAKE_TIMEOUT_MS = 2e4;
|
|
27487
|
+
var DEFAULT_RELAY_PING_INTERVAL_MS = 3 * 6e4;
|
|
27488
|
+
var DEFAULT_RELAY_PONG_TIMEOUT_MS = 3e4;
|
|
26378
27489
|
function connectRelayControl(options) {
|
|
26379
27490
|
const wsUrl = new URL(`${options.relayBaseUrl.replace(/\/+$/u, "")}/api/v1/relay/link/connect`);
|
|
26380
27491
|
wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -26383,10 +27494,15 @@ function connectRelayControl(options) {
|
|
|
26383
27494
|
const maxReconnectAttempts = options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY;
|
|
26384
27495
|
const backoffBaseMs = options.backoffBaseMs ?? DEFAULT_RELAY_RECONNECT_BASE_MS;
|
|
26385
27496
|
const backoffMaxMs = options.backoffMaxMs ?? DEFAULT_RELAY_RECONNECT_MAX_MS;
|
|
27497
|
+
const handshakeTimeoutMs = positiveInteger2(options.handshakeTimeoutMs, DEFAULT_RELAY_HANDSHAKE_TIMEOUT_MS);
|
|
27498
|
+
const pingIntervalMs = positiveInteger2(options.pingIntervalMs, DEFAULT_RELAY_PING_INTERVAL_MS);
|
|
27499
|
+
const pongTimeoutMs = positiveInteger2(options.pongTimeoutMs, DEFAULT_RELAY_PONG_TIMEOUT_MS);
|
|
26386
27500
|
let reconnectAttempts = 0;
|
|
26387
27501
|
let closedByUser = false;
|
|
26388
27502
|
let socket = null;
|
|
26389
27503
|
let retryTimer = null;
|
|
27504
|
+
let pingTimer = null;
|
|
27505
|
+
let pongTimer = null;
|
|
26390
27506
|
let abortControllers = /* @__PURE__ */ new Map();
|
|
26391
27507
|
let fatalRelayRejection = null;
|
|
26392
27508
|
let relayRetryAfterMs = null;
|
|
@@ -26414,15 +27530,19 @@ function connectRelayControl(options) {
|
|
|
26414
27530
|
});
|
|
26415
27531
|
};
|
|
26416
27532
|
const connect = () => {
|
|
27533
|
+
clearRetryTimer();
|
|
27534
|
+
clearHeartbeatTimers();
|
|
26417
27535
|
options.onStatus?.({ state: "connecting", attempt: reconnectAttempts });
|
|
26418
27536
|
fatalRelayRejection = null;
|
|
26419
27537
|
relayRetryAfterMs = null;
|
|
26420
27538
|
let closeHandled = false;
|
|
27539
|
+
let localCloseReason;
|
|
26421
27540
|
const handleConnectionClosed = (reason) => {
|
|
26422
27541
|
if (closeHandled) {
|
|
26423
27542
|
return;
|
|
26424
27543
|
}
|
|
26425
27544
|
closeHandled = true;
|
|
27545
|
+
clearHeartbeatTimers();
|
|
26426
27546
|
abortAll(abortControllers);
|
|
26427
27547
|
abortControllers = /* @__PURE__ */ new Map();
|
|
26428
27548
|
if (fatalRelayRejection) {
|
|
@@ -26449,30 +27569,48 @@ function connectRelayControl(options) {
|
|
|
26449
27569
|
scheduleTimer(backoffMaxMs, "retrying", `Relay reconnect scheduling failed: ${message}`);
|
|
26450
27570
|
});
|
|
26451
27571
|
};
|
|
26452
|
-
|
|
27572
|
+
const currentSocket = new WebSocket(wsUrl, {
|
|
27573
|
+
handshakeTimeout: handshakeTimeoutMs,
|
|
26453
27574
|
headers: {
|
|
26454
27575
|
"x-hermes-link-version": LINK_VERSION
|
|
26455
27576
|
}
|
|
26456
27577
|
});
|
|
26457
|
-
socket
|
|
27578
|
+
socket = currentSocket;
|
|
27579
|
+
currentSocket.on("open", () => {
|
|
27580
|
+
if (socket !== currentSocket) {
|
|
27581
|
+
return;
|
|
27582
|
+
}
|
|
26458
27583
|
reconnectAttempts = 0;
|
|
26459
27584
|
void clearRelayReconnectState(paths).catch(() => void 0);
|
|
26460
27585
|
options.onStatus?.({ state: "connected", attempt: reconnectAttempts });
|
|
26461
|
-
|
|
26462
|
-
|
|
27586
|
+
startHeartbeat(currentSocket, (message) => {
|
|
27587
|
+
localCloseReason = message;
|
|
27588
|
+
options.onStatus?.({ state: "disconnected", attempt: reconnectAttempts, message });
|
|
27589
|
+
currentSocket.terminate();
|
|
27590
|
+
});
|
|
27591
|
+
if (latestNetworkRoutes) {
|
|
26463
27592
|
sendNetworkRoutes(currentSocket, options.linkId, latestNetworkRoutes);
|
|
26464
27593
|
}
|
|
26465
27594
|
});
|
|
26466
|
-
|
|
26467
|
-
if (
|
|
27595
|
+
currentSocket.on("pong", () => {
|
|
27596
|
+
if (socket !== currentSocket) {
|
|
27597
|
+
return;
|
|
27598
|
+
}
|
|
27599
|
+
clearPongTimer();
|
|
27600
|
+
});
|
|
27601
|
+
currentSocket.on("message", (raw) => {
|
|
27602
|
+
if (socket !== currentSocket || typeof raw !== "string" && !Buffer.isBuffer(raw)) {
|
|
26468
27603
|
return;
|
|
26469
27604
|
}
|
|
26470
|
-
void handleFrame(
|
|
27605
|
+
void handleFrame(currentSocket, String(raw), options.localPort, abortControllers, streamBatchPolicy).catch((error) => {
|
|
26471
27606
|
const message = error instanceof Error ? error.message : "Relay request failed";
|
|
26472
|
-
|
|
27607
|
+
currentSocket.send(JSON.stringify({ type: "http.error", id: "unknown", status: 502, message }));
|
|
26473
27608
|
});
|
|
26474
27609
|
});
|
|
26475
|
-
|
|
27610
|
+
currentSocket.on("unexpected-response", (request, response) => {
|
|
27611
|
+
if (socket !== currentSocket) {
|
|
27612
|
+
return;
|
|
27613
|
+
}
|
|
26476
27614
|
const statusCode = response.statusCode ?? 0;
|
|
26477
27615
|
fatalRelayRejection = resolveFatalRelayRejectionFromStatus(statusCode);
|
|
26478
27616
|
relayRetryAfterMs = readRetryAfterMs(response);
|
|
@@ -26486,7 +27624,10 @@ function connectRelayControl(options) {
|
|
|
26486
27624
|
handleConnectionClosed(message);
|
|
26487
27625
|
request.destroy();
|
|
26488
27626
|
});
|
|
26489
|
-
|
|
27627
|
+
currentSocket.on("error", (error) => {
|
|
27628
|
+
if (socket !== currentSocket) {
|
|
27629
|
+
return;
|
|
27630
|
+
}
|
|
26490
27631
|
const message = error instanceof Error ? error.message : "Relay websocket error";
|
|
26491
27632
|
fatalRelayRejection = resolveFatalRelayRejection(message);
|
|
26492
27633
|
options.onStatus?.({
|
|
@@ -26495,8 +27636,12 @@ function connectRelayControl(options) {
|
|
|
26495
27636
|
message: fatalRelayRejection ?? message
|
|
26496
27637
|
});
|
|
26497
27638
|
});
|
|
26498
|
-
|
|
26499
|
-
|
|
27639
|
+
currentSocket.on("close", (code, reason) => {
|
|
27640
|
+
if (socket !== currentSocket) {
|
|
27641
|
+
return;
|
|
27642
|
+
}
|
|
27643
|
+
socket = null;
|
|
27644
|
+
handleConnectionClosed(localCloseReason ?? formatCloseReason(code, reason));
|
|
26500
27645
|
});
|
|
26501
27646
|
};
|
|
26502
27647
|
startConnect();
|
|
@@ -26526,10 +27671,59 @@ function connectRelayControl(options) {
|
|
|
26526
27671
|
return await readRelayCooldownDelayMs(paths).catch(() => 0);
|
|
26527
27672
|
}
|
|
26528
27673
|
function scheduleTimer(delay4, state, message) {
|
|
27674
|
+
clearRetryTimer();
|
|
26529
27675
|
options.onStatus?.({ state, attempt: reconnectAttempts, message });
|
|
26530
27676
|
retryTimer = setTimeout(connect, delay4);
|
|
26531
27677
|
retryTimer.unref?.();
|
|
26532
27678
|
}
|
|
27679
|
+
function clearRetryTimer() {
|
|
27680
|
+
if (retryTimer == null) {
|
|
27681
|
+
return;
|
|
27682
|
+
}
|
|
27683
|
+
clearTimeout(retryTimer);
|
|
27684
|
+
retryTimer = null;
|
|
27685
|
+
}
|
|
27686
|
+
function startHeartbeat(currentSocket, onTimeout) {
|
|
27687
|
+
clearHeartbeatTimers();
|
|
27688
|
+
pingTimer = setInterval(() => {
|
|
27689
|
+
if (socket !== currentSocket || currentSocket.readyState !== WebSocket.OPEN) {
|
|
27690
|
+
clearHeartbeatTimers();
|
|
27691
|
+
return;
|
|
27692
|
+
}
|
|
27693
|
+
if (pongTimer != null) {
|
|
27694
|
+
return;
|
|
27695
|
+
}
|
|
27696
|
+
try {
|
|
27697
|
+
currentSocket.ping();
|
|
27698
|
+
} catch {
|
|
27699
|
+
onTimeout("Relay websocket ping failed");
|
|
27700
|
+
return;
|
|
27701
|
+
}
|
|
27702
|
+
pongTimer = setTimeout(() => {
|
|
27703
|
+
if (socket !== currentSocket || currentSocket.readyState !== WebSocket.OPEN) {
|
|
27704
|
+
clearPongTimer();
|
|
27705
|
+
return;
|
|
27706
|
+
}
|
|
27707
|
+
onTimeout(`Relay websocket pong timed out after ${pongTimeoutMs}ms`);
|
|
27708
|
+
}, pongTimeoutMs);
|
|
27709
|
+
pongTimer.unref?.();
|
|
27710
|
+
}, pingIntervalMs);
|
|
27711
|
+
pingTimer.unref?.();
|
|
27712
|
+
}
|
|
27713
|
+
function clearHeartbeatTimers() {
|
|
27714
|
+
if (pingTimer != null) {
|
|
27715
|
+
clearInterval(pingTimer);
|
|
27716
|
+
pingTimer = null;
|
|
27717
|
+
}
|
|
27718
|
+
clearPongTimer();
|
|
27719
|
+
}
|
|
27720
|
+
function clearPongTimer() {
|
|
27721
|
+
if (pongTimer == null) {
|
|
27722
|
+
return;
|
|
27723
|
+
}
|
|
27724
|
+
clearTimeout(pongTimer);
|
|
27725
|
+
pongTimer = null;
|
|
27726
|
+
}
|
|
26533
27727
|
return {
|
|
26534
27728
|
publishNetworkRoutes(routes) {
|
|
26535
27729
|
latestNetworkRoutes = routes;
|
|
@@ -26543,10 +27737,8 @@ function connectRelayControl(options) {
|
|
|
26543
27737
|
},
|
|
26544
27738
|
close() {
|
|
26545
27739
|
closedByUser = true;
|
|
26546
|
-
|
|
26547
|
-
|
|
26548
|
-
retryTimer = null;
|
|
26549
|
-
}
|
|
27740
|
+
clearRetryTimer();
|
|
27741
|
+
clearHeartbeatTimers();
|
|
26550
27742
|
abortAll(abortControllers);
|
|
26551
27743
|
socket?.terminate();
|
|
26552
27744
|
}
|
|
@@ -26598,6 +27790,16 @@ function readRetryAfterMs(response) {
|
|
|
26598
27790
|
}
|
|
26599
27791
|
return Math.max(0, dateMs - Date.now());
|
|
26600
27792
|
}
|
|
27793
|
+
function formatCloseReason(code, reason) {
|
|
27794
|
+
const text = reason.toString("utf8").trim();
|
|
27795
|
+
if (code === 1e3 && !text) {
|
|
27796
|
+
return void 0;
|
|
27797
|
+
}
|
|
27798
|
+
return text ? `Relay websocket closed (${code}): ${text}` : `Relay websocket closed (${code})`;
|
|
27799
|
+
}
|
|
27800
|
+
function positiveInteger2(value, fallback) {
|
|
27801
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : fallback;
|
|
27802
|
+
}
|
|
26601
27803
|
async function handleFrame(socket, raw, localPort, abortControllers, streamBatchPolicy) {
|
|
26602
27804
|
const frame = JSON.parse(raw);
|
|
26603
27805
|
if (frame.type === "relay.config.update") {
|
|
@@ -26723,8 +27925,7 @@ async function readRelayStatusSnapshot(paths) {
|
|
|
26723
27925
|
return normalizeRelayStatusSnapshot(state.relayStatus);
|
|
26724
27926
|
}
|
|
26725
27927
|
async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new Date()) {
|
|
26726
|
-
|
|
26727
|
-
await writeJsonFile(paths.stateFile, {
|
|
27928
|
+
await updateLinkState(paths, (current) => ({
|
|
26728
27929
|
...current,
|
|
26729
27930
|
relayStatus: {
|
|
26730
27931
|
state: status.state,
|
|
@@ -26732,7 +27933,10 @@ async function writeRelayStatusSnapshot(paths, status, now = /* @__PURE__ */ new
|
|
|
26732
27933
|
message: normalizeMessage(status.message),
|
|
26733
27934
|
updatedAt: now.toISOString()
|
|
26734
27935
|
}
|
|
26735
|
-
});
|
|
27936
|
+
}));
|
|
27937
|
+
}
|
|
27938
|
+
async function updateLinkState(paths, update) {
|
|
27939
|
+
await updateJsonFile(paths.stateFile, (state) => update(state && typeof state === "object" ? state : {}));
|
|
26736
27940
|
}
|
|
26737
27941
|
async function readLinkState2(paths) {
|
|
26738
27942
|
const state = await readJsonFile(paths.stateFile);
|
|
@@ -27151,12 +28355,10 @@ async function mergeLastReportedPublicRoutes(paths, snapshotInput) {
|
|
|
27151
28355
|
};
|
|
27152
28356
|
}
|
|
27153
28357
|
async function updateNetworkReportState(paths, update) {
|
|
27154
|
-
|
|
27155
|
-
const next = {
|
|
28358
|
+
await updateJsonFile(paths.stateFile, (state) => ({
|
|
27156
28359
|
...state,
|
|
27157
|
-
networkReport: update(normalizeNetworkReportState(state
|
|
27158
|
-
};
|
|
27159
|
-
await writeJsonFile(paths.stateFile, next);
|
|
28360
|
+
networkReport: update(normalizeNetworkReportState(state?.networkReport))
|
|
28361
|
+
}));
|
|
27160
28362
|
}
|
|
27161
28363
|
async function readLinkState3(paths) {
|
|
27162
28364
|
const state = await readJsonFile(paths.stateFile);
|
|
@@ -27277,7 +28479,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
27277
28479
|
public_ipv6s: routes.publicIpv6s,
|
|
27278
28480
|
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
27279
28481
|
};
|
|
27280
|
-
const signature = signIdentityPayload(identity,
|
|
28482
|
+
const signature = signIdentityPayload(identity, canonicalJson2(payload));
|
|
27281
28483
|
const fetcher = options.fetchImpl ?? fetch;
|
|
27282
28484
|
const response = await fetcher(
|
|
27283
28485
|
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
|
|
@@ -27296,30 +28498,30 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
27296
28498
|
);
|
|
27297
28499
|
const body = await response.json().catch(() => null);
|
|
27298
28500
|
if (!response.ok || !body) {
|
|
27299
|
-
const message =
|
|
28501
|
+
const message = readErrorMessage4(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
27300
28502
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
27301
28503
|
}
|
|
27302
28504
|
await markNetworkStatusReported(paths, routes);
|
|
27303
28505
|
return body;
|
|
27304
28506
|
}
|
|
27305
|
-
function
|
|
27306
|
-
return JSON.stringify(
|
|
28507
|
+
function canonicalJson2(value) {
|
|
28508
|
+
return JSON.stringify(sortJsonValue2(value));
|
|
27307
28509
|
}
|
|
27308
|
-
function
|
|
28510
|
+
function sortJsonValue2(value) {
|
|
27309
28511
|
if (Array.isArray(value)) {
|
|
27310
|
-
return value.map(
|
|
28512
|
+
return value.map(sortJsonValue2);
|
|
27311
28513
|
}
|
|
27312
28514
|
if (value && typeof value === "object") {
|
|
27313
28515
|
const record = value;
|
|
27314
28516
|
const sorted = {};
|
|
27315
28517
|
for (const key of Object.keys(record).sort()) {
|
|
27316
|
-
sorted[key] =
|
|
28518
|
+
sorted[key] = sortJsonValue2(record[key]);
|
|
27317
28519
|
}
|
|
27318
28520
|
return sorted;
|
|
27319
28521
|
}
|
|
27320
28522
|
return value;
|
|
27321
28523
|
}
|
|
27322
|
-
function
|
|
28524
|
+
function readErrorMessage4(payload) {
|
|
27323
28525
|
if (typeof payload !== "object" || payload === null) {
|
|
27324
28526
|
return null;
|
|
27325
28527
|
}
|
|
@@ -27515,20 +28717,65 @@ function wait(ms) {
|
|
|
27515
28717
|
}
|
|
27516
28718
|
|
|
27517
28719
|
// src/daemon/scheduler.ts
|
|
28720
|
+
import { watch } from "fs";
|
|
27518
28721
|
function startCronDeliveryScheduler(options) {
|
|
27519
28722
|
let running = false;
|
|
27520
28723
|
let current = Promise.resolve();
|
|
28724
|
+
let debounceTimer = null;
|
|
28725
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
28726
|
+
const refreshWatchers = async () => {
|
|
28727
|
+
const dirs = await listHermesLinkCronOutputWatchDirs(options.paths).catch(
|
|
28728
|
+
(error) => {
|
|
28729
|
+
void options.logger.warn("cron_link_delivery_watch_failed", {
|
|
28730
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28731
|
+
});
|
|
28732
|
+
return [];
|
|
28733
|
+
}
|
|
28734
|
+
);
|
|
28735
|
+
const nextDirs = new Set(dirs);
|
|
28736
|
+
for (const [dir, watcher] of watchers) {
|
|
28737
|
+
if (!nextDirs.has(dir)) {
|
|
28738
|
+
watcher.close();
|
|
28739
|
+
watchers.delete(dir);
|
|
28740
|
+
}
|
|
28741
|
+
}
|
|
28742
|
+
for (const dir of nextDirs) {
|
|
28743
|
+
if (watchers.has(dir)) {
|
|
28744
|
+
continue;
|
|
28745
|
+
}
|
|
28746
|
+
try {
|
|
28747
|
+
const watcher = watch(
|
|
28748
|
+
dir,
|
|
28749
|
+
{ persistent: false, recursive: true },
|
|
28750
|
+
() => {
|
|
28751
|
+
triggerSync();
|
|
28752
|
+
}
|
|
28753
|
+
);
|
|
28754
|
+
watcher.on("error", (error) => {
|
|
28755
|
+
void options.logger.warn("cron_link_delivery_watch_failed", {
|
|
28756
|
+
dir,
|
|
28757
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28758
|
+
});
|
|
28759
|
+
watcher.close();
|
|
28760
|
+
watchers.delete(dir);
|
|
28761
|
+
});
|
|
28762
|
+
watchers.set(dir, watcher);
|
|
28763
|
+
} catch (error) {
|
|
28764
|
+
void options.logger.warn("cron_link_delivery_watch_failed", {
|
|
28765
|
+
dir,
|
|
28766
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28767
|
+
});
|
|
28768
|
+
}
|
|
28769
|
+
}
|
|
28770
|
+
};
|
|
27521
28771
|
const syncCronDeliveries = async () => {
|
|
27522
28772
|
if (running) {
|
|
27523
28773
|
return;
|
|
27524
28774
|
}
|
|
27525
28775
|
running = true;
|
|
27526
28776
|
try {
|
|
27527
|
-
await
|
|
27528
|
-
|
|
27529
|
-
options.conversations,
|
|
27530
|
-
options.logger
|
|
27531
|
-
);
|
|
28777
|
+
await options.conversations.syncCronDeliveries();
|
|
28778
|
+
await refreshWatchers();
|
|
27532
28779
|
} catch (error) {
|
|
27533
28780
|
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
27534
28781
|
source: "daemon_scheduler",
|
|
@@ -27538,13 +28785,32 @@ function startCronDeliveryScheduler(options) {
|
|
|
27538
28785
|
running = false;
|
|
27539
28786
|
}
|
|
27540
28787
|
};
|
|
28788
|
+
const triggerSync = () => {
|
|
28789
|
+
if (debounceTimer) {
|
|
28790
|
+
clearTimeout(debounceTimer);
|
|
28791
|
+
}
|
|
28792
|
+
debounceTimer = setTimeout(() => {
|
|
28793
|
+
debounceTimer = null;
|
|
28794
|
+
current = syncCronDeliveries();
|
|
28795
|
+
}, 500);
|
|
28796
|
+
debounceTimer.unref?.();
|
|
28797
|
+
};
|
|
27541
28798
|
const timer = setInterval(() => {
|
|
27542
28799
|
current = syncCronDeliveries();
|
|
27543
28800
|
}, options.intervalMs ?? 3e4);
|
|
27544
28801
|
timer.unref?.();
|
|
28802
|
+
void refreshWatchers();
|
|
28803
|
+
triggerSync();
|
|
27545
28804
|
return {
|
|
27546
28805
|
async close() {
|
|
27547
28806
|
clearInterval(timer);
|
|
28807
|
+
if (debounceTimer) {
|
|
28808
|
+
clearTimeout(debounceTimer);
|
|
28809
|
+
}
|
|
28810
|
+
for (const watcher of watchers.values()) {
|
|
28811
|
+
watcher.close();
|
|
28812
|
+
}
|
|
28813
|
+
watchers.clear();
|
|
27548
28814
|
await current.catch(() => void 0);
|
|
27549
28815
|
}
|
|
27550
28816
|
};
|
|
@@ -29258,7 +30524,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
29258
30524
|
}
|
|
29259
30525
|
const payload = await response.json().catch(() => null);
|
|
29260
30526
|
if (!response.ok) {
|
|
29261
|
-
const message =
|
|
30527
|
+
const message = readErrorMessage5(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
29262
30528
|
throw new Error(message);
|
|
29263
30529
|
}
|
|
29264
30530
|
if (!payload) {
|
|
@@ -29266,7 +30532,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
29266
30532
|
}
|
|
29267
30533
|
return payload;
|
|
29268
30534
|
}
|
|
29269
|
-
function
|
|
30535
|
+
function readErrorMessage5(payload) {
|
|
29270
30536
|
if (typeof payload !== "object" || payload === null) {
|
|
29271
30537
|
return null;
|
|
29272
30538
|
}
|
|
@@ -29600,12 +30866,12 @@ async function patchServerJson(serverBaseUrl, path29, token, body, options) {
|
|
|
29600
30866
|
async function readJsonResponse2(response) {
|
|
29601
30867
|
const payload = await response.json().catch(() => null);
|
|
29602
30868
|
if (!response.ok || !payload) {
|
|
29603
|
-
const message =
|
|
30869
|
+
const message = readErrorMessage6(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
29604
30870
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
29605
30871
|
}
|
|
29606
30872
|
return payload;
|
|
29607
30873
|
}
|
|
29608
|
-
function
|
|
30874
|
+
function readErrorMessage6(payload) {
|
|
29609
30875
|
if (typeof payload !== "object" || payload === null) {
|
|
29610
30876
|
return null;
|
|
29611
30877
|
}
|
|
@@ -29689,6 +30955,9 @@ function registerSystemRoutes(router, options) {
|
|
|
29689
30955
|
conversation_bulk_delete: true,
|
|
29690
30956
|
conversation_clear_plan: true,
|
|
29691
30957
|
conversation_cancel: true,
|
|
30958
|
+
conversation_queue_controls: true,
|
|
30959
|
+
conversation_queue_limit: MAX_CONVERSATION_QUEUED_RUNS,
|
|
30960
|
+
responses_interrupted_previous_response: true,
|
|
29692
30961
|
conversation_rename: true,
|
|
29693
30962
|
blobs: true,
|
|
29694
30963
|
devices: true,
|
|
@@ -29699,7 +30968,8 @@ function registerSystemRoutes(router, options) {
|
|
|
29699
30968
|
cron_jobs: true,
|
|
29700
30969
|
profile_skills: true,
|
|
29701
30970
|
profile_memory: true,
|
|
29702
|
-
hermes_updates: true
|
|
30971
|
+
hermes_updates: true,
|
|
30972
|
+
app_push_notification_events: true
|
|
29703
30973
|
}
|
|
29704
30974
|
};
|
|
29705
30975
|
});
|
|
@@ -30774,7 +32044,7 @@ async function createApp(options = {}) {
|
|
|
30774
32044
|
}
|
|
30775
32045
|
cronDeliverySyncRunning = true;
|
|
30776
32046
|
try {
|
|
30777
|
-
await
|
|
32047
|
+
await conversations.syncCronDeliveries();
|
|
30778
32048
|
} catch (error) {
|
|
30779
32049
|
void logger.warn("cron_link_delivery_sync_failed", {
|
|
30780
32050
|
source: "http_app_bootstrap",
|
|
@@ -30817,7 +32087,7 @@ async function createApp(options = {}) {
|
|
|
30817
32087
|
conversations,
|
|
30818
32088
|
syncCronDeliveries
|
|
30819
32089
|
});
|
|
30820
|
-
registerConversationRoutes(router, { paths, conversations });
|
|
32090
|
+
registerConversationRoutes(router, { paths, logger, conversations });
|
|
30821
32091
|
registerRunRoutes(router, { paths, logger, conversations });
|
|
30822
32092
|
registerProfileRoutes(router, { paths, logger, conversations });
|
|
30823
32093
|
app.use(router.routes());
|
|
@@ -30838,6 +32108,14 @@ export {
|
|
|
30838
32108
|
resolveHermesConfigPath,
|
|
30839
32109
|
ensureHermesApiServerConfig,
|
|
30840
32110
|
resolveRuntimePaths,
|
|
32111
|
+
defaultLinkConfig,
|
|
32112
|
+
loadConfig,
|
|
32113
|
+
saveConfig,
|
|
32114
|
+
parseLogLevel,
|
|
32115
|
+
normalizeLanHost,
|
|
32116
|
+
loadIdentity,
|
|
32117
|
+
ensureIdentity,
|
|
32118
|
+
getIdentityStatus,
|
|
30841
32119
|
createFileLogger,
|
|
30842
32120
|
getLinkLogFile,
|
|
30843
32121
|
readRecentLogEntries,
|
|
@@ -30849,14 +32127,6 @@ export {
|
|
|
30849
32127
|
ensureHermesApiServerAvailable,
|
|
30850
32128
|
readHermesVersion,
|
|
30851
32129
|
readHermesApiServerHealth,
|
|
30852
|
-
defaultLinkConfig,
|
|
30853
|
-
loadConfig,
|
|
30854
|
-
saveConfig,
|
|
30855
|
-
parseLogLevel,
|
|
30856
|
-
normalizeLanHost,
|
|
30857
|
-
loadIdentity,
|
|
30858
|
-
ensureIdentity,
|
|
30859
|
-
getIdentityStatus,
|
|
30860
32130
|
ConversationService,
|
|
30861
32131
|
hasActiveDevices,
|
|
30862
32132
|
prepareHermesProfilesForUse,
|