@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.
@@ -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
- runAt: output.mtime
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.8";
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 mkdir4(this.paths.logsDir, { recursive: true, mode: 448 });
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 mkdir4(paths.logsDir, { recursive: true, mode: 448 });
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 randomUUID3 } from "crypto";
7569
- import { mkdir as mkdir5, readFile as readFile6, readdir as readdir4, rm as rm3, stat as stat5, writeFile } from "fs/promises";
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_${randomUUID3().replaceAll("-", "")}`;
8318
+ const id = `blob_${randomUUID4().replaceAll("-", "")}`;
8023
8319
  const filePath = blobPath(paths, id);
8024
- await mkdir5(path9.dirname(filePath), { recursive: true, mode: 448 });
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 mkdir5(targetDir, { recursive: true, mode: 448 });
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 mkdir5(paths.blobsDir, { recursive: true, mode: 448 });
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 randomUUID4 } from "crypto";
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_${randomUUID4().replaceAll("-", "")}`,
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}_${randomUUID4().replaceAll("-", "").slice(0, 12)}`;
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 mkdir6, rm as rm4 } from "fs/promises";
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 mkdir6(directory, { recursive: true, mode: 448 });
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 randomUUID5 } from "crypto";
8961
- import { mkdir as mkdir7 } from "fs/promises";
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_${randomUUID5().replaceAll("-", "")}`,
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 mkdir7(this.plansDir(), { recursive: true, mode: 448 });
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 randomUUID6 } from "crypto";
9025
- import { mkdir as mkdir8 } from "fs/promises";
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_${randomUUID6().replaceAll("-", "")}`,
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 mkdir8(this.plansDir(), { recursive: true, mode: 448 });
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 randomUUID7 } from "crypto";
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_${randomUUID7().replaceAll("-", "")}`;
10738
+ return `msg_${randomUUID8().replaceAll("-", "")}`;
10517
10739
  }
10518
10740
  function createRunId() {
10519
- return `run_${randomUUID7().replaceAll("-", "")}`;
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 appendCommandResult(input) {
10559
- return this.appendCommandResultLocked(input);
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" ? readErrorMessage(input.payload) ?? actionSummary ?? void 0 : actionSummary ?? void 0;
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 readErrorMessage(payload) {
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 mkdir9,
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 mkdir9(this.paths.conversationsDir, { recursive: true, mode: 448 });
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 mkdir9(this.conversationDir(manifest.id), {
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 mkdir9(this.conversationDir(conversationId), {
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 randomUUID8 } from "crypto";
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 imported = await importHermesSession({
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 false;
12853
+ return null;
12437
12854
  });
12438
- if (imported) {
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 = lineageTitle(candidate) ?? readString9(candidate.session, "title") ?? firstUserText(snapshot);
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 true;
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
- return candidate.session._lineage_title ?? stripCompressionTitleSuffix2(readString9(candidate.session, "title") ?? "");
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 = lineageTitle(input.candidate);
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_${randomUUID8().replaceAll("-", "")}`,
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_${randomUUID8().replaceAll("-", "")}`;
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: readErrorMessage2(event.payload) ?? readDelta(event.payload) ?? "Hermes run failed"
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 null;
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 readErrorMessage2(payload) {
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(readErrorMessage2(event.payload));
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
- readErrorMessage2(input.event.payload) ?? "Hermes run failed",
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 input.fallbackInput;
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 content;
17771
+ return prefix ? `${prefix}
17772
+
17773
+ ${content}` : content;
17245
17774
  }
17246
- return `${content ? `${content}
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 ? readErrorMessage2(source.payload) ?? void 0 : void 0;
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((item) => item.status === "completed").filter((item) => item.hermes_response_id).filter((item) => item.hermes_session_id === run.hermes_session_id).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
- await this.withConversationLock(input.conversationId, async () => {
18502
- const manifest = await this.store.readActiveManifest(input.conversationId);
18503
- const snapshot = await this.store.readSnapshot(input.conversationId);
18504
- if (snapshot.messages.some(
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
- return;
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(manifest.id, snapshot);
18548
- await this.appendEvent(manifest.id, {
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(manifest.id, snapshot);
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
- await syncHermesLinkCronDeliveries(this.paths, this, this.logger);
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 createHash6 } from "crypto";
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 createHash6("sha256").update(value).digest("hex");
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
- return { kind: "device", device };
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, canonicalJson(payload));
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 = readErrorMessage3(body) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
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 canonicalJson(value) {
27481
- return JSON.stringify(sortJsonValue(value));
28507
+ function canonicalJson2(value) {
28508
+ return JSON.stringify(sortJsonValue2(value));
27482
28509
  }
27483
- function sortJsonValue(value) {
28510
+ function sortJsonValue2(value) {
27484
28511
  if (Array.isArray(value)) {
27485
- return value.map(sortJsonValue);
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] = sortJsonValue(record[key]);
28518
+ sorted[key] = sortJsonValue2(record[key]);
27492
28519
  }
27493
28520
  return sorted;
27494
28521
  }
27495
28522
  return value;
27496
28523
  }
27497
- function readErrorMessage3(payload) {
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 syncHermesLinkCronDeliveries(
27703
- options.paths,
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 = readErrorMessage4(payload) ?? `Relay request failed with HTTP ${response.status}`;
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 readErrorMessage4(payload) {
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 = readErrorMessage5(payload) ?? `HermesPilot Server request failed with HTTP ${response.status}`;
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 readErrorMessage5(payload) {
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 syncHermesLinkCronDeliveries(paths, conversations, logger);
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,