@hermespilot/link 0.6.8 → 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-57ZJLOQA.js → chunk-DOSXOXOS.js} +1378 -283
- 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";
|
|
@@ -5294,6 +5294,10 @@ async function bindCronJobToHermesLink(paths, input) {
|
|
|
5294
5294
|
if (existing) {
|
|
5295
5295
|
existing.conversationId = input.conversationId;
|
|
5296
5296
|
existing.source = input.source;
|
|
5297
|
+
if (input.ownerAccountId) {
|
|
5298
|
+
existing.ownerAccountId = input.ownerAccountId;
|
|
5299
|
+
existing.ownerAppInstanceId = input.ownerAppInstanceId;
|
|
5300
|
+
}
|
|
5297
5301
|
} else {
|
|
5298
5302
|
registry.bindings.push({
|
|
5299
5303
|
...input,
|
|
@@ -5303,6 +5307,26 @@ async function bindCronJobToHermesLink(paths, input) {
|
|
|
5303
5307
|
}
|
|
5304
5308
|
await writeRegistry(paths, registry);
|
|
5305
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
|
+
}
|
|
5306
5330
|
async function bindNewCronJobsToHermesLink(paths, input) {
|
|
5307
5331
|
for (const job of input.jobs) {
|
|
5308
5332
|
const jobId = readString3(job, "id") ?? readString3(job, "job_id");
|
|
@@ -5317,7 +5341,9 @@ async function bindNewCronJobsToHermesLink(paths, input) {
|
|
|
5317
5341
|
profileName: input.profileName,
|
|
5318
5342
|
jobId,
|
|
5319
5343
|
conversationId: input.conversationId,
|
|
5320
|
-
source: "natural_language"
|
|
5344
|
+
source: "natural_language",
|
|
5345
|
+
ownerAccountId: input.ownerAccountId,
|
|
5346
|
+
ownerAppInstanceId: input.ownerAppInstanceId
|
|
5321
5347
|
});
|
|
5322
5348
|
}
|
|
5323
5349
|
}
|
|
@@ -5342,6 +5368,28 @@ async function decorateHermesLinkCronJob(paths, profileName, job) {
|
|
|
5342
5368
|
);
|
|
5343
5369
|
return binding ? { ...job, deliver: HERMES_LINK_CRON_DELIVER } : job;
|
|
5344
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
|
+
}
|
|
5345
5393
|
async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
5346
5394
|
const registry = await readRegistry(paths);
|
|
5347
5395
|
let touched = false;
|
|
@@ -5358,15 +5406,27 @@ async function syncHermesLinkCronDeliveries(paths, runtime, logger) {
|
|
|
5358
5406
|
}
|
|
5359
5407
|
try {
|
|
5360
5408
|
const content = await readCronOutput(output.path);
|
|
5361
|
-
await runtime.appendCronDelivery({
|
|
5409
|
+
const handled = await runtime.appendCronDelivery({
|
|
5362
5410
|
conversationId: binding.conversationId,
|
|
5363
5411
|
profileName: binding.profileName,
|
|
5364
5412
|
jobId: binding.jobId,
|
|
5413
|
+
source: binding.source,
|
|
5365
5414
|
jobName: await readCronJobNameFromOutput(content),
|
|
5366
5415
|
outputPath: output.path,
|
|
5367
5416
|
content,
|
|
5368
|
-
|
|
5417
|
+
failed: isFailedCronOutput(content),
|
|
5418
|
+
runAt: output.mtime,
|
|
5419
|
+
accountId: binding.ownerAccountId,
|
|
5420
|
+
appInstanceId: binding.ownerAppInstanceId
|
|
5369
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
|
+
}
|
|
5370
5430
|
delivered.add(output.path);
|
|
5371
5431
|
touched = true;
|
|
5372
5432
|
} catch (error) {
|
|
@@ -5442,6 +5502,9 @@ async function readCronJobNameFromOutput(content) {
|
|
|
5442
5502
|
const match = content.match(/^#\s*Cron Job:\s*(.+)$/mu);
|
|
5443
5503
|
return match?.[1]?.trim() || void 0;
|
|
5444
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
|
+
}
|
|
5445
5508
|
async function readRegistry(paths) {
|
|
5446
5509
|
const existing = await readJsonFile(registryPath(paths));
|
|
5447
5510
|
if (existing?.version === REGISTRY_VERSION && Array.isArray(existing.bindings)) {
|
|
@@ -5468,7 +5531,7 @@ function normalizeDeliverValue(value) {
|
|
|
5468
5531
|
}
|
|
5469
5532
|
function isValidBinding(value) {
|
|
5470
5533
|
const binding = value;
|
|
5471
|
-
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");
|
|
5472
5535
|
}
|
|
5473
5536
|
function readString3(record, ...keys) {
|
|
5474
5537
|
for (const key of keys) {
|
|
@@ -5486,30 +5549,15 @@ function isConversationMissingError(error) {
|
|
|
5486
5549
|
return isLinkHttpError(error) && error.status === 404 && error.code === "conversation_not_found";
|
|
5487
5550
|
}
|
|
5488
5551
|
|
|
5489
|
-
// src/hermes/gateway.ts
|
|
5490
|
-
import { execFile as execFile2, spawn } from "child_process";
|
|
5491
|
-
import { constants as fsConstants } from "fs";
|
|
5492
|
-
import { access, readFile as readFile5, realpath, stat as stat4 } from "fs/promises";
|
|
5493
|
-
import path7 from "path";
|
|
5494
|
-
import { setTimeout as delay2 } from "timers/promises";
|
|
5495
|
-
import { promisify as promisify2 } from "util";
|
|
5496
|
-
|
|
5497
|
-
// src/runtime/logger.ts
|
|
5498
|
-
import { appendFile, mkdir as mkdir4, open as open2, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, truncate } from "fs/promises";
|
|
5499
|
-
import os3 from "os";
|
|
5500
|
-
import path6 from "path";
|
|
5501
|
-
|
|
5502
|
-
// src/runtime/paths.ts
|
|
5503
|
-
import os2 from "os";
|
|
5504
|
-
import path5 from "path";
|
|
5505
|
-
|
|
5506
5552
|
// src/constants.ts
|
|
5507
|
-
var LINK_VERSION = "0.6.
|
|
5553
|
+
var LINK_VERSION = "0.6.9";
|
|
5508
5554
|
var LINK_COMMAND = "hermeslink";
|
|
5509
5555
|
var LINK_DEFAULT_PORT = 52379;
|
|
5510
5556
|
var LINK_RUNTIME_DIR_NAME = ".hermeslink";
|
|
5511
5557
|
|
|
5512
5558
|
// src/runtime/paths.ts
|
|
5559
|
+
import os2 from "os";
|
|
5560
|
+
import path5 from "path";
|
|
5513
5561
|
function resolveRuntimeHome() {
|
|
5514
5562
|
return process.env.HERMESLINK_HOME?.trim() ? path5.resolve(process.env.HERMESLINK_HOME) : path5.join(os2.homedir(), LINK_RUNTIME_DIR_NAME);
|
|
5515
5563
|
}
|
|
@@ -5530,7 +5578,255 @@ function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
|
|
|
5530
5578
|
};
|
|
5531
5579
|
}
|
|
5532
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
|
+
|
|
5533
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";
|
|
5534
5830
|
var DEFAULT_LOG_FILE = "hermeslink.log";
|
|
5535
5831
|
var DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
5536
5832
|
var DEFAULT_MAX_FILES = 5;
|
|
@@ -5591,7 +5887,7 @@ var FileLogger = class {
|
|
|
5591
5887
|
return this.queue;
|
|
5592
5888
|
}
|
|
5593
5889
|
async appendEntry(entry) {
|
|
5594
|
-
await
|
|
5890
|
+
await mkdir5(this.paths.logsDir, { recursive: true, mode: 448 });
|
|
5595
5891
|
const line = `${JSON.stringify(entry)}
|
|
5596
5892
|
`;
|
|
5597
5893
|
await this.rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
@@ -5627,7 +5923,7 @@ function createRotatingTextLogWriter(options) {
|
|
|
5627
5923
|
return queue;
|
|
5628
5924
|
}
|
|
5629
5925
|
const next = queue.then(async () => {
|
|
5630
|
-
await
|
|
5926
|
+
await mkdir5(paths.logsDir, { recursive: true, mode: 448 });
|
|
5631
5927
|
await rotateLogFileIfNeeded(filePath, buffer.length, maxFileBytes, maxFiles);
|
|
5632
5928
|
await appendFile(filePath, buffer, { mode: 384 });
|
|
5633
5929
|
}).catch(() => void 0);
|
|
@@ -7565,8 +7861,8 @@ function firstRecord(...values) {
|
|
|
7565
7861
|
}
|
|
7566
7862
|
|
|
7567
7863
|
// src/conversations/blob-store.ts
|
|
7568
|
-
import { randomUUID as
|
|
7569
|
-
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";
|
|
7570
7866
|
import path9 from "path";
|
|
7571
7867
|
|
|
7572
7868
|
// src/conversations/media.ts
|
|
@@ -8019,9 +8315,9 @@ async function writeConversationBlob(paths, conversationId, input, options) {
|
|
|
8019
8315
|
if (input.bytes.byteLength > options.maxBytes) {
|
|
8020
8316
|
throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
|
|
8021
8317
|
}
|
|
8022
|
-
const id = `blob_${
|
|
8318
|
+
const id = `blob_${randomUUID4().replaceAll("-", "")}`;
|
|
8023
8319
|
const filePath = blobPath(paths, id);
|
|
8024
|
-
await
|
|
8320
|
+
await mkdir6(path9.dirname(filePath), { recursive: true, mode: 448 });
|
|
8025
8321
|
await writeFile(filePath, input.bytes, { mode: 384 });
|
|
8026
8322
|
const blob = {
|
|
8027
8323
|
id,
|
|
@@ -8101,7 +8397,7 @@ async function materializeConversationBlob(paths, conversationId, blobId, manife
|
|
|
8101
8397
|
targetDir,
|
|
8102
8398
|
materializedAttachmentFilename(blobId, manifest.filename ?? blobId)
|
|
8103
8399
|
);
|
|
8104
|
-
await
|
|
8400
|
+
await mkdir6(targetDir, { recursive: true, mode: 448 });
|
|
8105
8401
|
await writeFile(targetPath, await readFile6(blobPath(paths, blobId)), {
|
|
8106
8402
|
mode: 384
|
|
8107
8403
|
});
|
|
@@ -8131,7 +8427,7 @@ async function pruneConversationBlobReference(paths, conversationId, blobId) {
|
|
|
8131
8427
|
}
|
|
8132
8428
|
async function listConversationBlobIds(paths, conversationId) {
|
|
8133
8429
|
assertValidConversationId(conversationId);
|
|
8134
|
-
await
|
|
8430
|
+
await mkdir6(paths.blobsDir, { recursive: true, mode: 448 });
|
|
8135
8431
|
const entries = await readdir4(paths.blobsDir, {
|
|
8136
8432
|
withFileTypes: true
|
|
8137
8433
|
}).catch((error) => {
|
|
@@ -8359,6 +8655,9 @@ function hasRunningRuns(snapshot) {
|
|
|
8359
8655
|
function hasQueuedRuns(snapshot) {
|
|
8360
8656
|
return snapshot.runs.some((run) => run.status === "queued");
|
|
8361
8657
|
}
|
|
8658
|
+
function queuedRunCount(snapshot) {
|
|
8659
|
+
return snapshot.runs.filter((run) => run.status === "queued").length;
|
|
8660
|
+
}
|
|
8362
8661
|
function buildConversationEventStreamState(snapshot) {
|
|
8363
8662
|
const pendingApprovalRunIds = /* @__PURE__ */ new Set();
|
|
8364
8663
|
let hasPendingApproval = false;
|
|
@@ -8456,7 +8755,7 @@ function isRealtimeRunStatus(status) {
|
|
|
8456
8755
|
}
|
|
8457
8756
|
|
|
8458
8757
|
// src/conversations/slash-commands.ts
|
|
8459
|
-
import { randomUUID as
|
|
8758
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
8460
8759
|
var MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,127}$/u;
|
|
8461
8760
|
function isValidModelId(value) {
|
|
8462
8761
|
return MODEL_ID_PATTERN.test(value);
|
|
@@ -8553,7 +8852,7 @@ function parseSlashCommandInput(content) {
|
|
|
8553
8852
|
}
|
|
8554
8853
|
function createSlashCommandUserMessage(input) {
|
|
8555
8854
|
return {
|
|
8556
|
-
id: `msg_${
|
|
8855
|
+
id: `msg_${randomUUID5().replaceAll("-", "")}`,
|
|
8557
8856
|
schema_version: 1,
|
|
8558
8857
|
conversation_id: input.conversationId,
|
|
8559
8858
|
role: "user",
|
|
@@ -8587,7 +8886,7 @@ function slashHelpMessage() {
|
|
|
8587
8886
|
].join("\n");
|
|
8588
8887
|
}
|
|
8589
8888
|
function freshHermesSessionId(conversationId) {
|
|
8590
|
-
return `hp_${conversationId}_${
|
|
8889
|
+
return `hp_${conversationId}_${randomUUID5().replaceAll("-", "").slice(0, 12)}`;
|
|
8591
8890
|
}
|
|
8592
8891
|
function nextVerboseMode(current) {
|
|
8593
8892
|
const modes = [
|
|
@@ -8928,11 +9227,11 @@ function formatContextUsageLines(runtime) {
|
|
|
8928
9227
|
}
|
|
8929
9228
|
|
|
8930
9229
|
// src/conversations/delivery-staging.ts
|
|
8931
|
-
import { mkdir as
|
|
9230
|
+
import { mkdir as mkdir7, rm as rm4 } from "fs/promises";
|
|
8932
9231
|
import path10 from "path";
|
|
8933
9232
|
async function prepareDeliveryStagingRunDir(paths, conversationId, runId) {
|
|
8934
9233
|
const directory = deliveryStagingRunDir(paths, conversationId, runId);
|
|
8935
|
-
await
|
|
9234
|
+
await mkdir7(directory, { recursive: true, mode: 448 });
|
|
8936
9235
|
return directory;
|
|
8937
9236
|
}
|
|
8938
9237
|
async function removeConversationDeliveryStaging(paths, conversationId) {
|
|
@@ -8957,8 +9256,8 @@ function safePathSegment(value, fallback) {
|
|
|
8957
9256
|
}
|
|
8958
9257
|
|
|
8959
9258
|
// src/conversations/conversation-archive-plans.ts
|
|
8960
|
-
import { randomUUID as
|
|
8961
|
-
import { mkdir as
|
|
9259
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9260
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
8962
9261
|
import path11 from "path";
|
|
8963
9262
|
var PLAN_ID_PATTERN = /^archive_[a-f0-9]{32}$/u;
|
|
8964
9263
|
var ConversationArchivePlanStore = class {
|
|
@@ -8969,7 +9268,7 @@ var ConversationArchivePlanStore = class {
|
|
|
8969
9268
|
async create(conversationIds) {
|
|
8970
9269
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8971
9270
|
const plan = {
|
|
8972
|
-
id: `archive_${
|
|
9271
|
+
id: `archive_${randomUUID6().replaceAll("-", "")}`,
|
|
8973
9272
|
status: "prepared",
|
|
8974
9273
|
created_at: now,
|
|
8975
9274
|
updated_at: now,
|
|
@@ -8998,7 +9297,7 @@ var ConversationArchivePlanStore = class {
|
|
|
8998
9297
|
}
|
|
8999
9298
|
async write(plan) {
|
|
9000
9299
|
const normalizedPlanId = normalizePlanId(plan.id);
|
|
9001
|
-
await
|
|
9300
|
+
await mkdir8(this.plansDir(), { recursive: true, mode: 448 });
|
|
9002
9301
|
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
9003
9302
|
}
|
|
9004
9303
|
plansDir() {
|
|
@@ -9021,8 +9320,8 @@ function normalizePlanId(planId) {
|
|
|
9021
9320
|
}
|
|
9022
9321
|
|
|
9023
9322
|
// src/conversations/conversation-clear-plans.ts
|
|
9024
|
-
import { randomUUID as
|
|
9025
|
-
import { mkdir as
|
|
9323
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9324
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
9026
9325
|
import path12 from "path";
|
|
9027
9326
|
var PLAN_ID_PATTERN2 = /^clear_[a-f0-9]{32}$/u;
|
|
9028
9327
|
var ConversationClearPlanStore = class {
|
|
@@ -9033,7 +9332,7 @@ var ConversationClearPlanStore = class {
|
|
|
9033
9332
|
async create(conversationIds, targetStatus = "active") {
|
|
9034
9333
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9035
9334
|
const plan = {
|
|
9036
|
-
id: `clear_${
|
|
9335
|
+
id: `clear_${randomUUID7().replaceAll("-", "")}`,
|
|
9037
9336
|
status: "prepared",
|
|
9038
9337
|
target_status: targetStatus,
|
|
9039
9338
|
created_at: now,
|
|
@@ -9063,7 +9362,7 @@ var ConversationClearPlanStore = class {
|
|
|
9063
9362
|
}
|
|
9064
9363
|
async write(plan) {
|
|
9065
9364
|
const normalizedPlanId = normalizePlanId2(plan.id);
|
|
9066
|
-
await
|
|
9365
|
+
await mkdir9(this.plansDir(), { recursive: true, mode: 448 });
|
|
9067
9366
|
await writeJsonFile(this.planPath(normalizedPlanId), plan);
|
|
9068
9367
|
}
|
|
9069
9368
|
plansDir() {
|
|
@@ -9737,86 +10036,6 @@ function readAttachmentWaveform(attachment) {
|
|
|
9737
10036
|
).filter((item) => item !== void 0).slice(0, 96);
|
|
9738
10037
|
}
|
|
9739
10038
|
|
|
9740
|
-
// src/config/config.ts
|
|
9741
|
-
var defaultLinkConfig = {
|
|
9742
|
-
port: LINK_DEFAULT_PORT,
|
|
9743
|
-
lanHost: null,
|
|
9744
|
-
serverBaseUrl: "https://hermes-server.clawpilot.me",
|
|
9745
|
-
relayBaseUrl: "https://hermes-relay.clawpilot.me",
|
|
9746
|
-
appConnectTokenIssuer: "https://hermes-server.clawpilot.me",
|
|
9747
|
-
appConnectTokenAudience: "hermes-link",
|
|
9748
|
-
language: "auto",
|
|
9749
|
-
logLevel: "warn"
|
|
9750
|
-
};
|
|
9751
|
-
async function loadConfig(paths = resolveRuntimePaths()) {
|
|
9752
|
-
const existing = await readJsonFile(paths.configFile);
|
|
9753
|
-
const language = normalizeConfiguredLanguage(existing?.language);
|
|
9754
|
-
const lanHost = normalizeLanHost(existing?.lanHost);
|
|
9755
|
-
const logLevel = normalizeLogLevel(
|
|
9756
|
-
existing?.logLevel ?? process.env.HERMESLINK_LOG_LEVEL
|
|
9757
|
-
);
|
|
9758
|
-
return {
|
|
9759
|
-
...defaultLinkConfig,
|
|
9760
|
-
...existing ?? {},
|
|
9761
|
-
language,
|
|
9762
|
-
lanHost,
|
|
9763
|
-
logLevel
|
|
9764
|
-
};
|
|
9765
|
-
}
|
|
9766
|
-
async function saveConfig(patch, paths = resolveRuntimePaths()) {
|
|
9767
|
-
const current = await loadConfig(paths);
|
|
9768
|
-
const next = {
|
|
9769
|
-
...current,
|
|
9770
|
-
...patch,
|
|
9771
|
-
logLevel: patch.logLevel === void 0 ? current.logLevel : normalizeLogLevel(patch.logLevel)
|
|
9772
|
-
};
|
|
9773
|
-
await writeJsonFile(paths.configFile, next);
|
|
9774
|
-
return next;
|
|
9775
|
-
}
|
|
9776
|
-
function normalizeConfiguredLanguage(language) {
|
|
9777
|
-
if (language === "zh-CN" || language === "en" || language === "auto") {
|
|
9778
|
-
return language;
|
|
9779
|
-
}
|
|
9780
|
-
return defaultLinkConfig.language;
|
|
9781
|
-
}
|
|
9782
|
-
function normalizeLogLevel(level) {
|
|
9783
|
-
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
9784
|
-
return level;
|
|
9785
|
-
}
|
|
9786
|
-
return defaultLinkConfig.logLevel;
|
|
9787
|
-
}
|
|
9788
|
-
function parseLogLevel(value) {
|
|
9789
|
-
if (value === "debug" || value === "info" || value === "warn" || value === "error") {
|
|
9790
|
-
return value;
|
|
9791
|
-
}
|
|
9792
|
-
return null;
|
|
9793
|
-
}
|
|
9794
|
-
function normalizeLanHost(value) {
|
|
9795
|
-
if (value === null || value === void 0) {
|
|
9796
|
-
return null;
|
|
9797
|
-
}
|
|
9798
|
-
if (typeof value !== "string") {
|
|
9799
|
-
return null;
|
|
9800
|
-
}
|
|
9801
|
-
const host = value.trim().replace(/^\[/u, "").replace(/\]$/u, "");
|
|
9802
|
-
if (!host) {
|
|
9803
|
-
return null;
|
|
9804
|
-
}
|
|
9805
|
-
if (!isUsableLanIpv4(host)) {
|
|
9806
|
-
return null;
|
|
9807
|
-
}
|
|
9808
|
-
return host;
|
|
9809
|
-
}
|
|
9810
|
-
function isUsableLanIpv4(value) {
|
|
9811
|
-
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
|
|
9812
|
-
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
9813
|
-
return false;
|
|
9814
|
-
}
|
|
9815
|
-
const [first, second, , fourth] = parts;
|
|
9816
|
-
const privateRange = first === 10 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168;
|
|
9817
|
-
return privateRange && fourth !== 0 && fourth !== 255;
|
|
9818
|
-
}
|
|
9819
|
-
|
|
9820
10039
|
// src/hermes/session-title.ts
|
|
9821
10040
|
import { stat as stat7 } from "fs/promises";
|
|
9822
10041
|
import path13 from "path";
|
|
@@ -10279,7 +10498,7 @@ function stripCompressionTitleSuffix(value) {
|
|
|
10279
10498
|
}
|
|
10280
10499
|
|
|
10281
10500
|
// src/conversations/conversation-turns.ts
|
|
10282
|
-
import { randomUUID as
|
|
10501
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
10283
10502
|
var MESSAGE_ORDER_STEP_MS = 10;
|
|
10284
10503
|
var ASSISTANT_ORDER_OFFSET_MS = 1;
|
|
10285
10504
|
function createAgentTurnDraft(input) {
|
|
@@ -10294,6 +10513,7 @@ function createAgentTurnDraft(input) {
|
|
|
10294
10513
|
conversation_id: input.manifest.id,
|
|
10295
10514
|
role: "user",
|
|
10296
10515
|
status: shouldQueue ? "queued" : "completed",
|
|
10516
|
+
run_id: runId,
|
|
10297
10517
|
client_message_id: input.idempotencyKey,
|
|
10298
10518
|
created_at: userCreatedAt,
|
|
10299
10519
|
updated_at: now,
|
|
@@ -10337,6 +10557,8 @@ function createAgentTurnDraft(input) {
|
|
|
10337
10557
|
profile_uid: input.runtime.profileUid,
|
|
10338
10558
|
profile_name_snapshot: input.runtime.profileName,
|
|
10339
10559
|
profile: input.runtime.profileName,
|
|
10560
|
+
owner_account_id: input.accountId,
|
|
10561
|
+
owner_app_instance_id: input.appInstanceId,
|
|
10340
10562
|
model: input.runtime.model,
|
|
10341
10563
|
provider: input.runtime.provider,
|
|
10342
10564
|
context_window: input.runtime.contextWindow
|
|
@@ -10513,10 +10735,10 @@ function createAssistantMessage(input) {
|
|
|
10513
10735
|
};
|
|
10514
10736
|
}
|
|
10515
10737
|
function createMessageId() {
|
|
10516
|
-
return `msg_${
|
|
10738
|
+
return `msg_${randomUUID8().replaceAll("-", "")}`;
|
|
10517
10739
|
}
|
|
10518
10740
|
function createRunId() {
|
|
10519
|
-
return `run_${
|
|
10741
|
+
return `run_${randomUUID8().replaceAll("-", "")}`;
|
|
10520
10742
|
}
|
|
10521
10743
|
function hasActiveOrQueuedRuns(snapshot) {
|
|
10522
10744
|
return snapshot.runs.some(
|
|
@@ -10528,6 +10750,9 @@ function validTimestampOrNow(value) {
|
|
|
10528
10750
|
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
|
10529
10751
|
}
|
|
10530
10752
|
|
|
10753
|
+
// src/conversations/queue-policy.ts
|
|
10754
|
+
var MAX_CONVERSATION_QUEUED_RUNS = 10;
|
|
10755
|
+
|
|
10531
10756
|
// src/conversations/conversation-orchestration.ts
|
|
10532
10757
|
var ConversationOrchestrationCoordinator = class {
|
|
10533
10758
|
constructor(deps) {
|
|
@@ -10553,10 +10778,51 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10553
10778
|
if (!next) {
|
|
10554
10779
|
return;
|
|
10555
10780
|
}
|
|
10556
|
-
this.startRunWorkerAndDrain(next.conversationId, next.runId, next.input);
|
|
10781
|
+
this.startRunWorkerAndDrain(next.conversationId, next.runId, next.input);
|
|
10782
|
+
}
|
|
10783
|
+
async appendCommandResult(input) {
|
|
10784
|
+
return this.appendCommandResultLocked(input);
|
|
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
|
+
};
|
|
10557
10820
|
}
|
|
10558
|
-
async
|
|
10559
|
-
return this.
|
|
10821
|
+
async cancelQueuedRun(conversationId, runId) {
|
|
10822
|
+
return this.deps.withConversationLock(
|
|
10823
|
+
conversationId,
|
|
10824
|
+
() => this.cancelQueuedRunLocked(conversationId, runId)
|
|
10825
|
+
);
|
|
10560
10826
|
}
|
|
10561
10827
|
startRunWorkerAndDrain(conversationId, runId, input) {
|
|
10562
10828
|
void this.deps.runLifecycle.startRunWorker(conversationId, runId, input).catch(async (error) => {
|
|
@@ -10727,6 +10993,13 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10727
10993
|
}
|
|
10728
10994
|
}
|
|
10729
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
|
+
}
|
|
10730
11003
|
const { userMessage, assistantMessage, run, shouldQueue } = createAgentTurnDraft({
|
|
10731
11004
|
manifest,
|
|
10732
11005
|
snapshot,
|
|
@@ -10734,14 +11007,27 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10734
11007
|
content,
|
|
10735
11008
|
attachments: userAttachmentParts,
|
|
10736
11009
|
rawAttachments: input.attachments ?? [],
|
|
10737
|
-
idempotencyKey
|
|
11010
|
+
idempotencyKey,
|
|
11011
|
+
accountId: input.accountId,
|
|
11012
|
+
appInstanceId: input.appInstanceId
|
|
10738
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
|
+
}
|
|
10739
11022
|
const assistantMessageId = run.assistant_message_id;
|
|
10740
11023
|
snapshot.messages.push(
|
|
10741
11024
|
...assistantMessage ? [userMessage, assistantMessage] : [userMessage]
|
|
10742
11025
|
);
|
|
10743
11026
|
snapshot.runs.push(run);
|
|
10744
11027
|
await this.deps.store.writeSnapshot(manifest.id, snapshot);
|
|
11028
|
+
if (input.accountId) {
|
|
11029
|
+
await this.deps.store.writeManifest(manifest);
|
|
11030
|
+
}
|
|
10745
11031
|
manifest = await this.deps.metadata.applyTemporaryTitleFromFirstMessage(
|
|
10746
11032
|
manifest,
|
|
10747
11033
|
snapshot,
|
|
@@ -10879,6 +11165,137 @@ var ConversationOrchestrationCoordinator = class {
|
|
|
10879
11165
|
return this.deps.commandHandlers.restartGatewayFromCommand(input);
|
|
10880
11166
|
}
|
|
10881
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
|
+
}
|
|
10882
11299
|
async resetConversationContextLocked(input) {
|
|
10883
11300
|
if (hasRunningRuns(input.snapshot)) {
|
|
10884
11301
|
return this.appendCommandResultLocked({
|
|
@@ -11165,7 +11582,7 @@ function projectAgentEvent(input) {
|
|
|
11165
11582
|
summary,
|
|
11166
11583
|
args
|
|
11167
11584
|
});
|
|
11168
|
-
const detail = status === "failed" ?
|
|
11585
|
+
const detail = status === "failed" ? readErrorMessage2(input.payload) ?? actionSummary ?? void 0 : actionSummary ?? void 0;
|
|
11169
11586
|
const subtitle = actionSummary ?? (status === "running" ? `\u6B63\u5728\u8C03\u7528 ${name}` : status === "completed" ? `${name} \u5DF2\u5B8C\u6210` : `${name} \u6267\u884C\u5931\u8D25`);
|
|
11170
11587
|
return {
|
|
11171
11588
|
id,
|
|
@@ -11462,7 +11879,7 @@ function stableStringify(value) {
|
|
|
11462
11879
|
function hashAgentEventKey(value) {
|
|
11463
11880
|
return createHash3("sha256").update(value).digest("hex").slice(0, 16);
|
|
11464
11881
|
}
|
|
11465
|
-
function
|
|
11882
|
+
function readErrorMessage2(payload) {
|
|
11466
11883
|
if (typeof payload.error === "string" && payload.error.trim()) {
|
|
11467
11884
|
return payload.error.trim();
|
|
11468
11885
|
}
|
|
@@ -11817,7 +12234,7 @@ function hydrateAgentEventBlocks(blocks, agentEvents) {
|
|
|
11817
12234
|
// src/conversations/conversation-store.ts
|
|
11818
12235
|
import {
|
|
11819
12236
|
appendFile as appendFile2,
|
|
11820
|
-
mkdir as
|
|
12237
|
+
mkdir as mkdir10,
|
|
11821
12238
|
readdir as readdir5,
|
|
11822
12239
|
readFile as readFile7,
|
|
11823
12240
|
rm as rm5,
|
|
@@ -11830,7 +12247,7 @@ var ConversationStore = class {
|
|
|
11830
12247
|
}
|
|
11831
12248
|
paths;
|
|
11832
12249
|
async ensureConversationsDir() {
|
|
11833
|
-
await
|
|
12250
|
+
await mkdir10(this.paths.conversationsDir, { recursive: true, mode: 448 });
|
|
11834
12251
|
}
|
|
11835
12252
|
async listConversationIds() {
|
|
11836
12253
|
await this.ensureConversationsDir();
|
|
@@ -11845,7 +12262,7 @@ var ConversationStore = class {
|
|
|
11845
12262
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
11846
12263
|
}
|
|
11847
12264
|
async createConversation(manifest, snapshot = createEmptySnapshot2()) {
|
|
11848
|
-
await
|
|
12265
|
+
await mkdir10(this.conversationDir(manifest.id), {
|
|
11849
12266
|
recursive: true,
|
|
11850
12267
|
mode: 448
|
|
11851
12268
|
});
|
|
@@ -11885,7 +12302,7 @@ var ConversationStore = class {
|
|
|
11885
12302
|
conversation_id: conversationId,
|
|
11886
12303
|
created_at: now
|
|
11887
12304
|
};
|
|
11888
|
-
await
|
|
12305
|
+
await mkdir10(this.conversationDir(conversationId), {
|
|
11889
12306
|
recursive: true,
|
|
11890
12307
|
mode: 448
|
|
11891
12308
|
});
|
|
@@ -11981,7 +12398,7 @@ function isNodeError9(error, code) {
|
|
|
11981
12398
|
}
|
|
11982
12399
|
|
|
11983
12400
|
// src/conversations/hermes-session-sync.ts
|
|
11984
|
-
import { randomUUID as
|
|
12401
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
11985
12402
|
import { readdir as readdir7, readFile as readFile9, stat as stat9 } from "fs/promises";
|
|
11986
12403
|
import path16 from "path";
|
|
11987
12404
|
|
|
@@ -12423,7 +12840,7 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12423
12840
|
result.skipped_existing += 1;
|
|
12424
12841
|
continue;
|
|
12425
12842
|
}
|
|
12426
|
-
const
|
|
12843
|
+
const importedConversationId = await importHermesSession({
|
|
12427
12844
|
paths,
|
|
12428
12845
|
store,
|
|
12429
12846
|
logger,
|
|
@@ -12433,9 +12850,9 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12433
12850
|
profile: candidate.profileName,
|
|
12434
12851
|
message: error instanceof Error ? error.message : String(error)
|
|
12435
12852
|
});
|
|
12436
|
-
return
|
|
12853
|
+
return null;
|
|
12437
12854
|
});
|
|
12438
|
-
if (
|
|
12855
|
+
if (importedConversationId) {
|
|
12439
12856
|
result.imported_count += 1;
|
|
12440
12857
|
for (const sessionId of candidateSessionIds) {
|
|
12441
12858
|
knownHermesSessions.sessionIds.add(sessionId);
|
|
@@ -12449,6 +12866,80 @@ async function syncHermesSessionsIntoConversations(paths, logger, options = {})
|
|
|
12449
12866
|
}
|
|
12450
12867
|
return result;
|
|
12451
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
|
+
}
|
|
12452
12943
|
async function importHermesSession(input) {
|
|
12453
12944
|
const { paths, store, logger, candidate } = input;
|
|
12454
12945
|
const profile = await resolveConversationProfileTarget(
|
|
@@ -12474,7 +12965,7 @@ async function importHermesSession(input) {
|
|
|
12474
12965
|
}),
|
|
12475
12966
|
runs: []
|
|
12476
12967
|
};
|
|
12477
|
-
const title =
|
|
12968
|
+
const title = deriveHermesConversationTitle(candidate, snapshot);
|
|
12478
12969
|
const importedStats = buildImportedHermesStats({
|
|
12479
12970
|
candidate,
|
|
12480
12971
|
snapshot,
|
|
@@ -12541,7 +13032,7 @@ async function importHermesSession(input) {
|
|
|
12541
13032
|
paths,
|
|
12542
13033
|
toStatsIndexRecord(await store.readManifest(conversationId), stats)
|
|
12543
13034
|
);
|
|
12544
|
-
return
|
|
13035
|
+
return conversationId;
|
|
12545
13036
|
}
|
|
12546
13037
|
async function mergeExistingHermesConversation(input) {
|
|
12547
13038
|
const conversations = await readExistingHermesConversations(
|
|
@@ -12650,7 +13141,17 @@ function lineageSessionIds(candidate) {
|
|
|
12650
13141
|
]);
|
|
12651
13142
|
}
|
|
12652
13143
|
function lineageTitle(candidate) {
|
|
12653
|
-
|
|
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);
|
|
12654
13155
|
}
|
|
12655
13156
|
function lineageManifestPatch(candidate) {
|
|
12656
13157
|
const sessionIds = lineageSessionIds(candidate);
|
|
@@ -12803,7 +13304,7 @@ function mergeHermesLineageIntoManifest(input) {
|
|
|
12803
13304
|
profile: input.manifest.profile ?? input.manifest.profile_name_snapshot ?? input.profileName,
|
|
12804
13305
|
updated_at: latestTimestamp(input.manifest.updated_at, input.updatedAt)
|
|
12805
13306
|
};
|
|
12806
|
-
const title =
|
|
13307
|
+
const title = deriveHermesConversationTitle(input.candidate, input.snapshot);
|
|
12807
13308
|
if (title && canSyncHermesTitle(input.manifest)) {
|
|
12808
13309
|
nextBase.title = normalizeTitle(title);
|
|
12809
13310
|
nextBase.title_source = "hermes";
|
|
@@ -13370,6 +13871,15 @@ async function discoverHermesProfileNames() {
|
|
|
13370
13871
|
});
|
|
13371
13872
|
}
|
|
13372
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) {
|
|
13373
13883
|
if (!await isFile(dbPath)) {
|
|
13374
13884
|
return [];
|
|
13375
13885
|
}
|
|
@@ -13394,14 +13904,54 @@ async function listProfileSessions(dbPath) {
|
|
|
13394
13904
|
`
|
|
13395
13905
|
SELECT ${selectColumns}, ${lastActiveSql}
|
|
13396
13906
|
FROM sessions s
|
|
13907
|
+
${filter?.whereSql ?? ""}
|
|
13397
13908
|
ORDER BY last_active DESC
|
|
13398
13909
|
`
|
|
13399
|
-
).all();
|
|
13910
|
+
).all(...filter?.params ?? []);
|
|
13400
13911
|
return projectCompressionTips(rows);
|
|
13401
13912
|
} finally {
|
|
13402
13913
|
db?.close();
|
|
13403
13914
|
}
|
|
13404
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
|
+
}
|
|
13405
13955
|
function appendHermesRawMessage(message, row) {
|
|
13406
13956
|
const rows = readHermesRawMessageRows(message.raw);
|
|
13407
13957
|
message.raw = rows.length === 0 ? {
|
|
@@ -13751,7 +14301,7 @@ function toLinkMessage(input) {
|
|
|
13751
14301
|
const sessionId = readString9(input.message, "session_id") ?? input.sessionId;
|
|
13752
14302
|
const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
|
|
13753
14303
|
return {
|
|
13754
|
-
id: `msg_${
|
|
14304
|
+
id: `msg_${randomUUID9().replaceAll("-", "")}`,
|
|
13755
14305
|
schema_version: 1,
|
|
13756
14306
|
conversation_id: input.conversationId,
|
|
13757
14307
|
role,
|
|
@@ -13803,6 +14353,17 @@ function normalizeTitle(value) {
|
|
|
13803
14353
|
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
13804
14354
|
return normalized || DEFAULT_CONVERSATION_TITLE;
|
|
13805
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
|
+
}
|
|
13806
14367
|
function canSyncHermesTitle(manifest) {
|
|
13807
14368
|
return manifest.title_source !== "manual" && manifest.title_source !== "generated" && manifest.title_source !== "temporary_user_message" && manifest.title_source !== "temporary_fallback";
|
|
13808
14369
|
}
|
|
@@ -13923,7 +14484,7 @@ async function isFile(filePath) {
|
|
|
13923
14484
|
});
|
|
13924
14485
|
}
|
|
13925
14486
|
function createConversationId() {
|
|
13926
|
-
return `conv_${
|
|
14487
|
+
return `conv_${randomUUID9().replaceAll("-", "")}`;
|
|
13927
14488
|
}
|
|
13928
14489
|
function isoFromHermesTime(value) {
|
|
13929
14490
|
const numeric = readNumber2(value);
|
|
@@ -15322,71 +15883,6 @@ function compactProcessOutput(value) {
|
|
|
15322
15883
|
return compact || null;
|
|
15323
15884
|
}
|
|
15324
15885
|
|
|
15325
|
-
// src/identity/identity.ts
|
|
15326
|
-
import { generateKeyPairSync, randomUUID as randomUUID9, sign } from "crypto";
|
|
15327
|
-
import { mkdir as mkdir10, chmod as chmod2 } from "fs/promises";
|
|
15328
|
-
import { z } from "zod";
|
|
15329
|
-
var linkIdentitySchema = z.object({
|
|
15330
|
-
install_id: z.string().min(1),
|
|
15331
|
-
link_id: z.string().min(1).nullable().optional(),
|
|
15332
|
-
public_key_pem: z.string().min(1),
|
|
15333
|
-
private_key_pem: z.string().min(1),
|
|
15334
|
-
created_at: z.string().min(1),
|
|
15335
|
-
updated_at: z.string().min(1)
|
|
15336
|
-
});
|
|
15337
|
-
async function loadIdentity(paths = resolveRuntimePaths()) {
|
|
15338
|
-
const value = await readJsonFile(paths.identityFile);
|
|
15339
|
-
if (value === null) {
|
|
15340
|
-
return null;
|
|
15341
|
-
}
|
|
15342
|
-
return linkIdentitySchema.parse(value);
|
|
15343
|
-
}
|
|
15344
|
-
async function ensureIdentity(paths = resolveRuntimePaths()) {
|
|
15345
|
-
const existing = await loadIdentity(paths);
|
|
15346
|
-
if (existing) {
|
|
15347
|
-
return existing;
|
|
15348
|
-
}
|
|
15349
|
-
await mkdir10(paths.homeDir, { recursive: true, mode: 448 });
|
|
15350
|
-
await chmod2(paths.homeDir, 448).catch(() => void 0);
|
|
15351
|
-
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
15352
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15353
|
-
const identity = {
|
|
15354
|
-
install_id: `install_${randomUUID9().replaceAll("-", "")}`,
|
|
15355
|
-
link_id: null,
|
|
15356
|
-
public_key_pem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
15357
|
-
private_key_pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
15358
|
-
created_at: now,
|
|
15359
|
-
updated_at: now
|
|
15360
|
-
};
|
|
15361
|
-
await writeJsonFile(paths.identityFile, identity);
|
|
15362
|
-
return identity;
|
|
15363
|
-
}
|
|
15364
|
-
async function saveAssignedLinkId(linkId, paths = resolveRuntimePaths()) {
|
|
15365
|
-
const identity = await ensureIdentity(paths);
|
|
15366
|
-
const next = {
|
|
15367
|
-
...identity,
|
|
15368
|
-
link_id: linkId,
|
|
15369
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15370
|
-
};
|
|
15371
|
-
await writeJsonFile(paths.identityFile, next);
|
|
15372
|
-
return next;
|
|
15373
|
-
}
|
|
15374
|
-
function signRelayNonce(identity, nonce) {
|
|
15375
|
-
return signIdentityPayload(identity, nonce);
|
|
15376
|
-
}
|
|
15377
|
-
function signIdentityPayload(identity, payload) {
|
|
15378
|
-
const signature = sign(null, Buffer.from(payload, "utf8"), identity.private_key_pem);
|
|
15379
|
-
return signature.toString("base64url");
|
|
15380
|
-
}
|
|
15381
|
-
function getIdentityStatus(identity) {
|
|
15382
|
-
return {
|
|
15383
|
-
installId: identity.install_id,
|
|
15384
|
-
linkId: identity.link_id ?? null,
|
|
15385
|
-
hasPrivateKey: identity.private_key_pem.trim().length > 0,
|
|
15386
|
-
publicKeyPem: identity.public_key_pem
|
|
15387
|
-
};
|
|
15388
|
-
}
|
|
15389
|
-
|
|
15390
15886
|
// src/conversations/hermes-sse.ts
|
|
15391
15887
|
async function* parseSseResponse(response) {
|
|
15392
15888
|
if (!response.body) {
|
|
@@ -16068,7 +16564,7 @@ function normalizeHermesStreamEvent(event) {
|
|
|
16068
16564
|
...event.payload,
|
|
16069
16565
|
type: "run.failed",
|
|
16070
16566
|
error: {
|
|
16071
|
-
message:
|
|
16567
|
+
message: readErrorMessage3(event.payload) ?? readDelta(event.payload) ?? "Hermes run failed"
|
|
16072
16568
|
}
|
|
16073
16569
|
}
|
|
16074
16570
|
};
|
|
@@ -16144,11 +16640,24 @@ function normalizeHermesResponseEvent(event) {
|
|
|
16144
16640
|
} : null;
|
|
16145
16641
|
}
|
|
16146
16642
|
case "response.created":
|
|
16147
|
-
return
|
|
16643
|
+
return normalizeResponseCreated(event);
|
|
16148
16644
|
default:
|
|
16149
16645
|
return null;
|
|
16150
16646
|
}
|
|
16151
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
|
+
}
|
|
16152
16661
|
function normalizeResponseOutputItemAdded(event) {
|
|
16153
16662
|
const item = toRecord12(event.payload.item);
|
|
16154
16663
|
if (readString14(item, "type") !== "function_call") {
|
|
@@ -16245,7 +16754,7 @@ function normalizeStreamingTextDelta(currentText, nextChunk) {
|
|
|
16245
16754
|
}
|
|
16246
16755
|
return nextChunk;
|
|
16247
16756
|
}
|
|
16248
|
-
function
|
|
16757
|
+
function readErrorMessage3(payload) {
|
|
16249
16758
|
if (typeof payload.error === "string" && payload.error.trim()) {
|
|
16250
16759
|
return payload.error.trim();
|
|
16251
16760
|
}
|
|
@@ -16309,7 +16818,7 @@ function isTopLevelErrorEvent(event) {
|
|
|
16309
16818
|
if (type.startsWith("tool.")) {
|
|
16310
16819
|
return false;
|
|
16311
16820
|
}
|
|
16312
|
-
return type === "error" || type === "run.error" || event.eventName === "error" || Boolean(
|
|
16821
|
+
return type === "error" || type === "run.error" || event.eventName === "error" || Boolean(readErrorMessage3(event.payload));
|
|
16313
16822
|
}
|
|
16314
16823
|
function readChatCompletionDelta(payload) {
|
|
16315
16824
|
const choice = readFirstChoice(payload);
|
|
@@ -16895,12 +17404,27 @@ var ConversationRunLifecycle = class {
|
|
|
16895
17404
|
this.deps.scheduleTitleRefresh(input.conversationId);
|
|
16896
17405
|
}
|
|
16897
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
|
+
}
|
|
16898
17417
|
if (input.event.payloadType === "run.completed") {
|
|
16899
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);
|
|
16900
17422
|
await this.bindNewCronJobsCreatedByRun({
|
|
16901
17423
|
profileName: input.profileName,
|
|
16902
17424
|
conversationId: input.conversationId,
|
|
16903
|
-
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
|
|
16904
17428
|
});
|
|
16905
17429
|
}
|
|
16906
17430
|
await this.deps.syncCronDeliveries().catch((error) => {
|
|
@@ -16958,7 +17482,7 @@ var ConversationRunLifecycle = class {
|
|
|
16958
17482
|
await this.failRun(
|
|
16959
17483
|
input.conversationId,
|
|
16960
17484
|
input.runId,
|
|
16961
|
-
|
|
17485
|
+
readErrorMessage3(input.event.payload) ?? "Hermes run failed",
|
|
16962
17486
|
input.event
|
|
16963
17487
|
);
|
|
16964
17488
|
return true;
|
|
@@ -17208,8 +17732,11 @@ var ConversationRunLifecycle = class {
|
|
|
17208
17732
|
const userMessage = input.snapshot.messages.find(
|
|
17209
17733
|
(message) => message.id === input.run.trigger_message_id
|
|
17210
17734
|
);
|
|
17735
|
+
const prefix = guidedInterruptInputPrefix(input.run);
|
|
17211
17736
|
if (!userMessage || !userMessage.parts.some(isVoicePart)) {
|
|
17212
|
-
return
|
|
17737
|
+
return prefix ? `${prefix}
|
|
17738
|
+
|
|
17739
|
+
${input.fallbackInput}` : input.fallbackInput;
|
|
17213
17740
|
}
|
|
17214
17741
|
const content = messageText(userMessage);
|
|
17215
17742
|
const voiceLines = [];
|
|
@@ -17241,11 +17768,16 @@ ${attachmentLines.join("\n")}`
|
|
|
17241
17768
|
);
|
|
17242
17769
|
}
|
|
17243
17770
|
if (sections.length === 0) {
|
|
17244
|
-
return
|
|
17771
|
+
return prefix ? `${prefix}
|
|
17772
|
+
|
|
17773
|
+
${content}` : content;
|
|
17245
17774
|
}
|
|
17246
|
-
|
|
17775
|
+
const resolved = `${content ? `${content}
|
|
17247
17776
|
|
|
17248
17777
|
` : ""}${sections.join("\n\n")}`;
|
|
17778
|
+
return prefix ? `${prefix}
|
|
17779
|
+
|
|
17780
|
+
${resolved}` : resolved;
|
|
17249
17781
|
}
|
|
17250
17782
|
async updateRun(conversationId, runId, patch) {
|
|
17251
17783
|
return this.deps.withConversationLock(
|
|
@@ -17666,6 +18198,13 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17666
18198
|
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
17667
18199
|
});
|
|
17668
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
|
+
});
|
|
17669
18208
|
}
|
|
17670
18209
|
async failRunLocked(conversationId, runId, message, source) {
|
|
17671
18210
|
const snapshot = await this.deps.readSnapshot(conversationId).catch(() => null);
|
|
@@ -17679,7 +18218,7 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17679
18218
|
run.status = "failed";
|
|
17680
18219
|
run.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
17681
18220
|
run.error_message = message;
|
|
17682
|
-
run.error_detail = source ?
|
|
18221
|
+
run.error_detail = source ? readErrorMessage3(source.payload) ?? void 0 : void 0;
|
|
17683
18222
|
const visibleMessage = formatFailureMessage(message, run.error_detail);
|
|
17684
18223
|
const usage = readUsage(source?.payload);
|
|
17685
18224
|
if (usage) {
|
|
@@ -17717,12 +18256,56 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17717
18256
|
...source ? { raw: { format: "hermes-run-event", payload: source.rawPayload } } : {}
|
|
17718
18257
|
});
|
|
17719
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
|
+
});
|
|
17720
18267
|
void this.deps.logger.warn("conversation_run_failed", {
|
|
17721
18268
|
conversation_id: conversationId,
|
|
17722
18269
|
run_id: runId,
|
|
17723
18270
|
error: message
|
|
17724
18271
|
});
|
|
17725
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
|
+
}
|
|
17726
18309
|
async cancelRunLocked(conversationId, runId, options) {
|
|
17727
18310
|
const manifest = await this.deps.readRunnableManifest(conversationId);
|
|
17728
18311
|
const snapshot = await this.deps.readSnapshot(conversationId);
|
|
@@ -17753,6 +18336,13 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17753
18336
|
});
|
|
17754
18337
|
}
|
|
17755
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
|
+
}
|
|
17756
18346
|
const cancelledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17757
18347
|
run.status = "cancelled";
|
|
17758
18348
|
run.completed_at = cancelledAt;
|
|
@@ -17838,7 +18428,9 @@ ${details.join("\n")}` : emptyHermesResponseMessage();
|
|
|
17838
18428
|
profileName,
|
|
17839
18429
|
conversationId: input.conversationId,
|
|
17840
18430
|
beforeJobIds: input.beforeJobIds,
|
|
17841
|
-
jobs
|
|
18431
|
+
jobs,
|
|
18432
|
+
ownerAccountId: input.ownerAccountId,
|
|
18433
|
+
ownerAppInstanceId: input.ownerAppInstanceId
|
|
17842
18434
|
});
|
|
17843
18435
|
}
|
|
17844
18436
|
};
|
|
@@ -17856,7 +18448,19 @@ function buildRunInstructions(run, deliveryStagingDir) {
|
|
|
17856
18448
|
"Current runtime selected by Hermes Link:",
|
|
17857
18449
|
`- Model: ${run.model ?? "hermes-agent"}`,
|
|
17858
18450
|
`- Provider: ${run.provider ?? "unknown"}`,
|
|
17859
|
-
"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."
|
|
17860
18464
|
].join("\n");
|
|
17861
18465
|
}
|
|
17862
18466
|
function appendMediaImportFailureNotice(message) {
|
|
@@ -18120,7 +18724,9 @@ function findPreviousHermesResponseId(snapshot, run) {
|
|
|
18120
18724
|
if (!currentProfile) {
|
|
18121
18725
|
return void 0;
|
|
18122
18726
|
}
|
|
18123
|
-
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(
|
|
18124
18730
|
(item) => normalizeRunProfileForCompare(
|
|
18125
18731
|
item.profile_name_snapshot ?? item.profile
|
|
18126
18732
|
) === currentProfile
|
|
@@ -18203,6 +18809,17 @@ function readStatusErrorMessage(value) {
|
|
|
18203
18809
|
function formatUnknownErrorMessage(error) {
|
|
18204
18810
|
return error instanceof Error ? error.message : String(error);
|
|
18205
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
|
+
}
|
|
18206
18823
|
async function closeSseIterator(iterator) {
|
|
18207
18824
|
if (!iterator.return) {
|
|
18208
18825
|
return;
|
|
@@ -18335,6 +18952,7 @@ var ConversationService = class {
|
|
|
18335
18952
|
queries;
|
|
18336
18953
|
runLifecycle;
|
|
18337
18954
|
hermesSessionSyncPromise = null;
|
|
18955
|
+
cronDeliverySyncPromise = null;
|
|
18338
18956
|
async withConversationLock(conversationId, task) {
|
|
18339
18957
|
const previous = this.conversationLocks.get(conversationId) ?? Promise.resolve();
|
|
18340
18958
|
let release;
|
|
@@ -18445,6 +19063,8 @@ var ConversationService = class {
|
|
|
18445
19063
|
profile_uid: profile.profileUid,
|
|
18446
19064
|
profile_name_snapshot: profile.profileName,
|
|
18447
19065
|
profile: profile.profileName,
|
|
19066
|
+
owner_account_id: input.accountId,
|
|
19067
|
+
owner_app_instance_id: input.appInstanceId,
|
|
18448
19068
|
created_at: now,
|
|
18449
19069
|
updated_at: now,
|
|
18450
19070
|
last_event_seq: 0
|
|
@@ -18498,19 +19118,115 @@ var ConversationService = class {
|
|
|
18498
19118
|
return created.id;
|
|
18499
19119
|
}
|
|
18500
19120
|
async appendCronDelivery(input) {
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
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(
|
|
18505
19191
|
(message2) => message2.hermes?.cron_output_path === input.outputPath
|
|
18506
|
-
)
|
|
18507
|
-
|
|
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
|
+
});
|
|
18508
19216
|
}
|
|
18509
19217
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18510
19218
|
const createdAt = input.runAt ?? now;
|
|
18511
19219
|
const profileName = normalizeProfileName2(
|
|
18512
19220
|
manifest.profile_name_snapshot ?? manifest.profile ?? input.profileName
|
|
18513
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;
|
|
18514
19230
|
const message = {
|
|
18515
19231
|
id: `msg_${randomUUID10().replaceAll("-", "")}`,
|
|
18516
19232
|
schema_version: 1,
|
|
@@ -18544,17 +19260,152 @@ var ConversationService = class {
|
|
|
18544
19260
|
}
|
|
18545
19261
|
};
|
|
18546
19262
|
snapshot.messages.push(message);
|
|
18547
|
-
await this.store.writeSnapshot(
|
|
18548
|
-
|
|
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, {
|
|
18549
19268
|
type: "message.created",
|
|
18550
19269
|
message_id: message.id,
|
|
18551
19270
|
payload: { message }
|
|
18552
19271
|
});
|
|
18553
|
-
await this.persistConversationStats(
|
|
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;
|
|
18554
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
|
+
}
|
|
19351
|
+
});
|
|
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
|
+
}
|
|
18555
19362
|
}
|
|
18556
19363
|
async syncCronDeliveries() {
|
|
18557
|
-
|
|
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
|
+
}
|
|
18558
19409
|
}
|
|
18559
19410
|
async syncHermesSessions() {
|
|
18560
19411
|
if (this.hermesSessionSyncPromise) {
|
|
@@ -18851,6 +19702,12 @@ var ConversationService = class {
|
|
|
18851
19702
|
}
|
|
18852
19703
|
return this.cancelRun(conversationId, runId);
|
|
18853
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
|
+
}
|
|
18854
19711
|
async resolveApproval(input) {
|
|
18855
19712
|
const decision = input.decision;
|
|
18856
19713
|
if (decision === "once" || decision === "session") {
|
|
@@ -19162,9 +20019,76 @@ function findApproval(snapshot, approvalId) {
|
|
|
19162
20019
|
}
|
|
19163
20020
|
return null;
|
|
19164
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
|
+
}
|
|
19165
20089
|
|
|
19166
20090
|
// src/security/devices.ts
|
|
19167
|
-
import { randomBytes as randomBytes2, randomUUID as randomUUID11, timingSafeEqual, createHash as
|
|
20091
|
+
import { randomBytes as randomBytes2, randomUUID as randomUUID11, timingSafeEqual, createHash as createHash7 } from "crypto";
|
|
19168
20092
|
var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
|
|
19169
20093
|
var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
19170
20094
|
var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
@@ -19460,7 +20384,7 @@ function randomToken(prefix) {
|
|
|
19460
20384
|
return `${prefix}${randomBytes2(24).toString("base64url")}`;
|
|
19461
20385
|
}
|
|
19462
20386
|
function sha256(value) {
|
|
19463
|
-
return
|
|
20387
|
+
return createHash7("sha256").update(value).digest("hex");
|
|
19464
20388
|
}
|
|
19465
20389
|
function safeEqual(left, right) {
|
|
19466
20390
|
const leftBytes = Buffer.from(left);
|
|
@@ -19540,7 +20464,8 @@ async function authenticateRequest(ctx, paths) {
|
|
|
19540
20464
|
}
|
|
19541
20465
|
const device = await authenticateDeviceAccessToken(token, paths);
|
|
19542
20466
|
if (device) {
|
|
19543
|
-
|
|
20467
|
+
const owner = await readOptionalAppConnectOwner(ctx, paths);
|
|
20468
|
+
return { kind: "device", device, ...owner };
|
|
19544
20469
|
}
|
|
19545
20470
|
if (token.startsWith("hpat_")) {
|
|
19546
20471
|
throw new LinkHttpError(
|
|
@@ -19564,6 +20489,31 @@ async function authenticateRequest(ctx, paths) {
|
|
|
19564
20489
|
appInstanceId: normalizeAppInstanceId2(claims.app_instance_id)
|
|
19565
20490
|
};
|
|
19566
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
|
+
}
|
|
19567
20517
|
async function loadRequiredIdentity(paths) {
|
|
19568
20518
|
const identity = await loadIdentity(paths);
|
|
19569
20519
|
if (!identity?.link_id) {
|
|
@@ -19579,6 +20529,16 @@ function readBearerToken(value) {
|
|
|
19579
20529
|
const token = trimmed.slice(7).trim();
|
|
19580
20530
|
return token || null;
|
|
19581
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
|
+
}
|
|
19582
20542
|
function normalizeScopes(value) {
|
|
19583
20543
|
if (Array.isArray(value)) {
|
|
19584
20544
|
return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
@@ -20013,7 +20973,8 @@ function isExpectedClientDisconnectError(error) {
|
|
|
20013
20973
|
function registerConversationRoutes(router, options) {
|
|
20014
20974
|
const { paths, logger, conversations } = options;
|
|
20015
20975
|
router.get("/api/v1/conversations", async (ctx) => {
|
|
20016
|
-
await authenticateRequest(ctx, paths);
|
|
20976
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20977
|
+
await prepareConversationListRead(conversations, logger, auth);
|
|
20017
20978
|
ctx.set("cache-control", "no-store");
|
|
20018
20979
|
const result = await conversations.listConversationPage({
|
|
20019
20980
|
limit: readLimit(ctx.query.limit),
|
|
@@ -20040,7 +21001,8 @@ function registerConversationRoutes(router, options) {
|
|
|
20040
21001
|
};
|
|
20041
21002
|
});
|
|
20042
21003
|
router.get("/api/v1/conversations/archived", async (ctx) => {
|
|
20043
|
-
await authenticateRequest(ctx, paths);
|
|
21004
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21005
|
+
await prepareConversationListRead(conversations, logger, auth);
|
|
20044
21006
|
ctx.set("cache-control", "no-store");
|
|
20045
21007
|
const result = await conversations.listArchivedConversationPage({
|
|
20046
21008
|
limit: readLimit(ctx.query.limit),
|
|
@@ -20067,14 +21029,16 @@ function registerConversationRoutes(router, options) {
|
|
|
20067
21029
|
};
|
|
20068
21030
|
});
|
|
20069
21031
|
router.post("/api/v1/conversations", async (ctx) => {
|
|
20070
|
-
await authenticateRequest(ctx, paths);
|
|
21032
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20071
21033
|
const body = await readJsonBody(ctx.req);
|
|
20072
21034
|
ctx.status = 201;
|
|
20073
21035
|
ctx.body = {
|
|
20074
21036
|
ok: true,
|
|
20075
21037
|
conversation: await conversations.createConversation({
|
|
20076
21038
|
title: readString16(body, "title") ?? void 0,
|
|
20077
|
-
profileName: readOptionalProfileName(body)
|
|
21039
|
+
profileName: readOptionalProfileName(body),
|
|
21040
|
+
accountId: auth.accountId,
|
|
21041
|
+
appInstanceId: auth.appInstanceId
|
|
20078
21042
|
})
|
|
20079
21043
|
};
|
|
20080
21044
|
});
|
|
@@ -20143,7 +21107,7 @@ function registerConversationRoutes(router, options) {
|
|
|
20143
21107
|
);
|
|
20144
21108
|
});
|
|
20145
21109
|
router.post("/api/v1/conversations/:conversationId/messages", async (ctx) => {
|
|
20146
|
-
await authenticateRequest(ctx, paths);
|
|
21110
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
20147
21111
|
const body = await readJsonBody(ctx.req);
|
|
20148
21112
|
const content = readString16(body, "content") ?? readString16(body, "text") ?? readString16(body, "input") ?? "";
|
|
20149
21113
|
const attachments = readMessageAttachments(body.attachments ?? body.blobs);
|
|
@@ -20163,7 +21127,9 @@ function registerConversationRoutes(router, options) {
|
|
|
20163
21127
|
attachments,
|
|
20164
21128
|
clientMessageId: readString16(body, "client_message_id") ?? readString16(body, "clientMessageId") ?? void 0,
|
|
20165
21129
|
idempotencyKey: readHeader(ctx, "idempotency-key") ?? void 0,
|
|
20166
|
-
profileName: readOptionalProfileName(body)
|
|
21130
|
+
profileName: readOptionalProfileName(body),
|
|
21131
|
+
accountId: auth.accountId,
|
|
21132
|
+
appInstanceId: auth.appInstanceId
|
|
20167
21133
|
})
|
|
20168
21134
|
};
|
|
20169
21135
|
});
|
|
@@ -20359,6 +21325,33 @@ function registerConversationRoutes(router, options) {
|
|
|
20359
21325
|
};
|
|
20360
21326
|
}
|
|
20361
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
|
+
);
|
|
20362
21355
|
router.post(
|
|
20363
21356
|
"/api/v1/conversations/:conversationId/approvals/:approvalId/approve",
|
|
20364
21357
|
async (ctx) => {
|
|
@@ -20472,6 +21465,22 @@ function registerConversationRoutes(router, options) {
|
|
|
20472
21465
|
}
|
|
20473
21466
|
);
|
|
20474
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
|
+
}
|
|
20475
21484
|
function resolveConversationEventCursor(input) {
|
|
20476
21485
|
const queryAfter = readInteger3(input.queryAfter) ?? 0;
|
|
20477
21486
|
const headerAfter = readNonNegativeIntegerHeader(input.lastEventIdHeader) ?? 0;
|
|
@@ -21054,8 +22063,8 @@ function toRecord14(value) {
|
|
|
21054
22063
|
function registerCronJobRoutes(router, options) {
|
|
21055
22064
|
const { paths, logger, conversations, syncCronDeliveries } = options;
|
|
21056
22065
|
router.get("/api/v1/cron-jobs", async (ctx) => {
|
|
21057
|
-
await authenticateRequest(ctx, paths);
|
|
21058
|
-
await syncCronDeliveries
|
|
22066
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
22067
|
+
await prepareCronJobRead(conversations, logger, auth, syncCronDeliveries);
|
|
21059
22068
|
ctx.set("cache-control", "no-store");
|
|
21060
22069
|
const includeDisabled = readQueryString(ctx.query.include_disabled)?.toLowerCase() === "true" || readQueryString(ctx.query.includeDisabled)?.toLowerCase() === "true";
|
|
21061
22070
|
const profiles = await listHermesProfiles(paths);
|
|
@@ -21094,8 +22103,8 @@ function registerCronJobRoutes(router, options) {
|
|
|
21094
22103
|
ctx.body = { ok: failures.length === 0, jobs, failures };
|
|
21095
22104
|
});
|
|
21096
22105
|
router.get("/api/v1/profiles/:name/cron-jobs", async (ctx) => {
|
|
21097
|
-
await authenticateRequest(ctx, paths);
|
|
21098
|
-
await syncCronDeliveries
|
|
22106
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
22107
|
+
await prepareCronJobRead(conversations, logger, auth, syncCronDeliveries);
|
|
21099
22108
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21100
22109
|
ctx.set("cache-control", "no-store");
|
|
21101
22110
|
const includeDisabled = readQueryString(ctx.query.include_disabled)?.toLowerCase() === "true" || readQueryString(ctx.query.includeDisabled)?.toLowerCase() === "true";
|
|
@@ -21114,7 +22123,7 @@ function registerCronJobRoutes(router, options) {
|
|
|
21114
22123
|
};
|
|
21115
22124
|
});
|
|
21116
22125
|
router.post("/api/v1/profiles/:name/cron-jobs", async (ctx) => {
|
|
21117
|
-
await authenticateRequest(ctx, paths);
|
|
22126
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21118
22127
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21119
22128
|
const body = await readJsonBody(ctx.req);
|
|
21120
22129
|
const input = readCronJobCreateInput(body);
|
|
@@ -21128,7 +22137,9 @@ function registerCronJobRoutes(router, options) {
|
|
|
21128
22137
|
conversations,
|
|
21129
22138
|
profileName: profile.name,
|
|
21130
22139
|
job,
|
|
21131
|
-
source: "app"
|
|
22140
|
+
source: "app",
|
|
22141
|
+
accountId: auth.accountId,
|
|
22142
|
+
appInstanceId: auth.appInstanceId
|
|
21132
22143
|
}) : job;
|
|
21133
22144
|
ctx.status = 201;
|
|
21134
22145
|
ctx.body = { ok: true, job: attachCronJobProfile(decoratedJob, profile) };
|
|
@@ -21150,7 +22161,7 @@ function registerCronJobRoutes(router, options) {
|
|
|
21150
22161
|
};
|
|
21151
22162
|
});
|
|
21152
22163
|
router.patch("/api/v1/profiles/:name/cron-jobs/:jobId", async (ctx) => {
|
|
21153
|
-
await authenticateRequest(ctx, paths);
|
|
22164
|
+
const auth = await authenticateRequest(ctx, paths);
|
|
21154
22165
|
const profile = await getHermesProfileStatus(ctx.params.name, paths);
|
|
21155
22166
|
const body = await readJsonBody(ctx.req);
|
|
21156
22167
|
const input = readCronJobUpdateInput(body);
|
|
@@ -21171,7 +22182,9 @@ function registerCronJobRoutes(router, options) {
|
|
|
21171
22182
|
conversations,
|
|
21172
22183
|
profileName: profile.name,
|
|
21173
22184
|
job,
|
|
21174
|
-
source: "app"
|
|
22185
|
+
source: "app",
|
|
22186
|
+
accountId: auth.accountId,
|
|
22187
|
+
appInstanceId: auth.appInstanceId
|
|
21175
22188
|
});
|
|
21176
22189
|
} else if (deliverTouched) {
|
|
21177
22190
|
await unbindCronJobFromHermesLink(paths, profile.name, ctx.params.jobId);
|
|
@@ -21236,6 +22249,18 @@ function registerCronJobRoutes(router, options) {
|
|
|
21236
22249
|
};
|
|
21237
22250
|
});
|
|
21238
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
|
+
}
|
|
21239
22264
|
function toHermesCronJobInput(input) {
|
|
21240
22265
|
return {
|
|
21241
22266
|
...input,
|
|
@@ -21247,14 +22272,16 @@ async function bindAndDecorateCronJobForHermesLink(input) {
|
|
|
21247
22272
|
if (!jobId) {
|
|
21248
22273
|
return input.job;
|
|
21249
22274
|
}
|
|
21250
|
-
const conversationId = await input.conversations.ensureCronInboxConversation({
|
|
22275
|
+
const conversationId = input.source === "natural_language" ? await input.conversations.ensureCronInboxConversation({
|
|
21251
22276
|
profileName: input.profileName
|
|
21252
|
-
});
|
|
22277
|
+
}) : void 0;
|
|
21253
22278
|
await bindCronJobToHermesLink(input.paths, {
|
|
21254
22279
|
profileName: input.profileName,
|
|
21255
22280
|
jobId,
|
|
21256
22281
|
conversationId,
|
|
21257
|
-
source: input.source
|
|
22282
|
+
source: input.source,
|
|
22283
|
+
ownerAccountId: input.accountId,
|
|
22284
|
+
ownerAppInstanceId: input.appInstanceId
|
|
21258
22285
|
});
|
|
21259
22286
|
return { ...input.job, deliver: HERMES_LINK_CRON_DELIVER };
|
|
21260
22287
|
}
|
|
@@ -27452,7 +28479,7 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
27452
28479
|
public_ipv6s: routes.publicIpv6s,
|
|
27453
28480
|
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
27454
28481
|
};
|
|
27455
|
-
const signature = signIdentityPayload(identity,
|
|
28482
|
+
const signature = signIdentityPayload(identity, canonicalJson2(payload));
|
|
27456
28483
|
const fetcher = options.fetchImpl ?? fetch;
|
|
27457
28484
|
const response = await fetcher(
|
|
27458
28485
|
`${config.serverBaseUrl.replace(/\/+$/u, "")}/api/v1/links/${encodeURIComponent(identity.link_id)}/report`,
|
|
@@ -27471,30 +28498,30 @@ async function reportLinkStatusToServer(options = {}) {
|
|
|
27471
28498
|
);
|
|
27472
28499
|
const body = await response.json().catch(() => null);
|
|
27473
28500
|
if (!response.ok || !body) {
|
|
27474
|
-
const message =
|
|
28501
|
+
const message = readErrorMessage4(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
27475
28502
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
27476
28503
|
}
|
|
27477
28504
|
await markNetworkStatusReported(paths, routes);
|
|
27478
28505
|
return body;
|
|
27479
28506
|
}
|
|
27480
|
-
function
|
|
27481
|
-
return JSON.stringify(
|
|
28507
|
+
function canonicalJson2(value) {
|
|
28508
|
+
return JSON.stringify(sortJsonValue2(value));
|
|
27482
28509
|
}
|
|
27483
|
-
function
|
|
28510
|
+
function sortJsonValue2(value) {
|
|
27484
28511
|
if (Array.isArray(value)) {
|
|
27485
|
-
return value.map(
|
|
28512
|
+
return value.map(sortJsonValue2);
|
|
27486
28513
|
}
|
|
27487
28514
|
if (value && typeof value === "object") {
|
|
27488
28515
|
const record = value;
|
|
27489
28516
|
const sorted = {};
|
|
27490
28517
|
for (const key of Object.keys(record).sort()) {
|
|
27491
|
-
sorted[key] =
|
|
28518
|
+
sorted[key] = sortJsonValue2(record[key]);
|
|
27492
28519
|
}
|
|
27493
28520
|
return sorted;
|
|
27494
28521
|
}
|
|
27495
28522
|
return value;
|
|
27496
28523
|
}
|
|
27497
|
-
function
|
|
28524
|
+
function readErrorMessage4(payload) {
|
|
27498
28525
|
if (typeof payload !== "object" || payload === null) {
|
|
27499
28526
|
return null;
|
|
27500
28527
|
}
|
|
@@ -27690,20 +28717,65 @@ function wait(ms) {
|
|
|
27690
28717
|
}
|
|
27691
28718
|
|
|
27692
28719
|
// src/daemon/scheduler.ts
|
|
28720
|
+
import { watch } from "fs";
|
|
27693
28721
|
function startCronDeliveryScheduler(options) {
|
|
27694
28722
|
let running = false;
|
|
27695
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
|
+
};
|
|
27696
28771
|
const syncCronDeliveries = async () => {
|
|
27697
28772
|
if (running) {
|
|
27698
28773
|
return;
|
|
27699
28774
|
}
|
|
27700
28775
|
running = true;
|
|
27701
28776
|
try {
|
|
27702
|
-
await
|
|
27703
|
-
|
|
27704
|
-
options.conversations,
|
|
27705
|
-
options.logger
|
|
27706
|
-
);
|
|
28777
|
+
await options.conversations.syncCronDeliveries();
|
|
28778
|
+
await refreshWatchers();
|
|
27707
28779
|
} catch (error) {
|
|
27708
28780
|
void options.logger.warn("cron_link_delivery_sync_failed", {
|
|
27709
28781
|
source: "daemon_scheduler",
|
|
@@ -27713,13 +28785,32 @@ function startCronDeliveryScheduler(options) {
|
|
|
27713
28785
|
running = false;
|
|
27714
28786
|
}
|
|
27715
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
|
+
};
|
|
27716
28798
|
const timer = setInterval(() => {
|
|
27717
28799
|
current = syncCronDeliveries();
|
|
27718
28800
|
}, options.intervalMs ?? 3e4);
|
|
27719
28801
|
timer.unref?.();
|
|
28802
|
+
void refreshWatchers();
|
|
28803
|
+
triggerSync();
|
|
27720
28804
|
return {
|
|
27721
28805
|
async close() {
|
|
27722
28806
|
clearInterval(timer);
|
|
28807
|
+
if (debounceTimer) {
|
|
28808
|
+
clearTimeout(debounceTimer);
|
|
28809
|
+
}
|
|
28810
|
+
for (const watcher of watchers.values()) {
|
|
28811
|
+
watcher.close();
|
|
28812
|
+
}
|
|
28813
|
+
watchers.clear();
|
|
27723
28814
|
await current.catch(() => void 0);
|
|
27724
28815
|
}
|
|
27725
28816
|
};
|
|
@@ -29433,7 +30524,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
29433
30524
|
}
|
|
29434
30525
|
const payload = await response.json().catch(() => null);
|
|
29435
30526
|
if (!response.ok) {
|
|
29436
|
-
const message =
|
|
30527
|
+
const message = readErrorMessage5(payload) ?? `Relay request failed with HTTP ${response.status}`;
|
|
29437
30528
|
throw new Error(message);
|
|
29438
30529
|
}
|
|
29439
30530
|
if (!payload) {
|
|
@@ -29441,7 +30532,7 @@ async function postJson(fetcher, url, token, body) {
|
|
|
29441
30532
|
}
|
|
29442
30533
|
return payload;
|
|
29443
30534
|
}
|
|
29444
|
-
function
|
|
30535
|
+
function readErrorMessage5(payload) {
|
|
29445
30536
|
if (typeof payload !== "object" || payload === null) {
|
|
29446
30537
|
return null;
|
|
29447
30538
|
}
|
|
@@ -29775,12 +30866,12 @@ async function patchServerJson(serverBaseUrl, path29, token, body, options) {
|
|
|
29775
30866
|
async function readJsonResponse2(response) {
|
|
29776
30867
|
const payload = await response.json().catch(() => null);
|
|
29777
30868
|
if (!response.ok || !payload) {
|
|
29778
|
-
const message =
|
|
30869
|
+
const message = readErrorMessage6(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
|
|
29779
30870
|
throw new LinkHttpError(response.status, "server_request_failed", message);
|
|
29780
30871
|
}
|
|
29781
30872
|
return payload;
|
|
29782
30873
|
}
|
|
29783
|
-
function
|
|
30874
|
+
function readErrorMessage6(payload) {
|
|
29784
30875
|
if (typeof payload !== "object" || payload === null) {
|
|
29785
30876
|
return null;
|
|
29786
30877
|
}
|
|
@@ -29864,6 +30955,9 @@ function registerSystemRoutes(router, options) {
|
|
|
29864
30955
|
conversation_bulk_delete: true,
|
|
29865
30956
|
conversation_clear_plan: true,
|
|
29866
30957
|
conversation_cancel: true,
|
|
30958
|
+
conversation_queue_controls: true,
|
|
30959
|
+
conversation_queue_limit: MAX_CONVERSATION_QUEUED_RUNS,
|
|
30960
|
+
responses_interrupted_previous_response: true,
|
|
29867
30961
|
conversation_rename: true,
|
|
29868
30962
|
blobs: true,
|
|
29869
30963
|
devices: true,
|
|
@@ -29874,7 +30968,8 @@ function registerSystemRoutes(router, options) {
|
|
|
29874
30968
|
cron_jobs: true,
|
|
29875
30969
|
profile_skills: true,
|
|
29876
30970
|
profile_memory: true,
|
|
29877
|
-
hermes_updates: true
|
|
30971
|
+
hermes_updates: true,
|
|
30972
|
+
app_push_notification_events: true
|
|
29878
30973
|
}
|
|
29879
30974
|
};
|
|
29880
30975
|
});
|
|
@@ -30949,7 +32044,7 @@ async function createApp(options = {}) {
|
|
|
30949
32044
|
}
|
|
30950
32045
|
cronDeliverySyncRunning = true;
|
|
30951
32046
|
try {
|
|
30952
|
-
await
|
|
32047
|
+
await conversations.syncCronDeliveries();
|
|
30953
32048
|
} catch (error) {
|
|
30954
32049
|
void logger.warn("cron_link_delivery_sync_failed", {
|
|
30955
32050
|
source: "http_app_bootstrap",
|
|
@@ -31013,6 +32108,14 @@ export {
|
|
|
31013
32108
|
resolveHermesConfigPath,
|
|
31014
32109
|
ensureHermesApiServerConfig,
|
|
31015
32110
|
resolveRuntimePaths,
|
|
32111
|
+
defaultLinkConfig,
|
|
32112
|
+
loadConfig,
|
|
32113
|
+
saveConfig,
|
|
32114
|
+
parseLogLevel,
|
|
32115
|
+
normalizeLanHost,
|
|
32116
|
+
loadIdentity,
|
|
32117
|
+
ensureIdentity,
|
|
32118
|
+
getIdentityStatus,
|
|
31016
32119
|
createFileLogger,
|
|
31017
32120
|
getLinkLogFile,
|
|
31018
32121
|
readRecentLogEntries,
|
|
@@ -31024,14 +32127,6 @@ export {
|
|
|
31024
32127
|
ensureHermesApiServerAvailable,
|
|
31025
32128
|
readHermesVersion,
|
|
31026
32129
|
readHermesApiServerHealth,
|
|
31027
|
-
defaultLinkConfig,
|
|
31028
|
-
loadConfig,
|
|
31029
|
-
saveConfig,
|
|
31030
|
-
parseLogLevel,
|
|
31031
|
-
normalizeLanHost,
|
|
31032
|
-
loadIdentity,
|
|
31033
|
-
ensureIdentity,
|
|
31034
|
-
getIdentityStatus,
|
|
31035
32130
|
ConversationService,
|
|
31036
32131
|
hasActiveDevices,
|
|
31037
32132
|
prepareHermesProfilesForUse,
|