@remnic/plugin-openclaw 9.46.0 → 9.46.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3114,14 +3114,14 @@ __reExport(access_http_exports, access_http_star);
3114
3114
  import * as access_http_star from "@remnic/core/access-http";
3115
3115
 
3116
3116
  // ../../src/index.ts
3117
- import path6 from "path";
3117
+ import path10 from "path";
3118
3118
  import os from "os";
3119
3119
 
3120
3120
  // ../../src/opik-exporter.ts
3121
3121
  import { createOpikExporter, OpikExporter } from "@remnic/core/opik-exporter";
3122
3122
 
3123
3123
  // ../../src/index.ts
3124
- import { readEnvVar, resolveHomeDir as resolveHomeDir2 } from "@remnic/core/runtime/env";
3124
+ import { readEnvVar, resolveHomeDir as resolveHomeDir3 } from "@remnic/core/runtime/env";
3125
3125
  import { displayErrorDetail as displayErrorDetail2 } from "@remnic/core/runtime/better-sqlite";
3126
3126
 
3127
3127
  // ../../src/migrate/from-engram.ts
@@ -5332,51 +5332,31 @@ function resolveRemnicOpenClawPluginEntry(raw, preferredId) {
5332
5332
  }
5333
5333
 
5334
5334
  // src/delegate-runtime.ts
5335
- import path4 from "path";
5335
+ import path8 from "path";
5336
5336
  import {
5337
5337
  renderMemoryContextPrompt
5338
5338
  } from "@remnic/core";
5339
+ import { log as log6 } from "@remnic/core/logger";
5340
+
5341
+ // src/delegate-authorization.ts
5339
5342
  import { log as log2 } from "@remnic/core/logger";
5340
- import {
5341
- SESSION_NAMESPACE_BINDING_MAX_ENTRIES,
5342
- SESSION_NAMESPACE_BINDING_MAX_NAMESPACES,
5343
- SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH,
5344
- createFileSessionNamespaceBindingStore
5345
- } from "@remnic/core/session-namespace-bindings";
5346
- import { createFileToggleStore } from "@remnic/core/session-toggles";
5347
5343
 
5348
5344
  // src/bridge.ts
5349
- import fs from "fs";
5350
- import path3 from "path";
5345
+ import fs3 from "fs";
5346
+ import path6 from "path";
5347
+ import { isIPv4, isIPv6 } from "net";
5351
5348
  import { Worker } from "worker_threads";
5352
- import { expandTildePath } from "@remnic/core";
5353
- var DEFAULT_HOST = "127.0.0.1";
5354
- var DEFAULT_PORT = 4318;
5355
- var LIVENESS_PATH = "/engram/v1/live";
5356
- var LEGACY_HEALTH_PATH = "/engram/v1/health";
5357
- var DEFAULT_DAEMON_HEALTH_TIMEOUT_MS = 1e4;
5358
- function parseBridgeHealthTimeoutMs(value) {
5359
- if (value === void 0) return DEFAULT_DAEMON_HEALTH_TIMEOUT_MS;
5360
- const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
5361
- if (typeof parsed !== "number" || !Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 12e4) {
5362
- throw new Error(
5363
- `bridgeHealthTimeoutMs must be an integer in [1, 120000]; got ${String(value)}`
5364
- );
5365
- }
5366
- return parsed;
5367
- }
5368
- function parseOpenClawBridgeConfig(config) {
5369
- return {
5370
- healthTimeoutMs: parseBridgeHealthTimeoutMs(config.bridgeHealthTimeoutMs)
5371
- };
5372
- }
5349
+ import { expandTildePath as expandTildePath3 } from "@remnic/core";
5350
+
5351
+ // src/bridge-health-worker.ts
5373
5352
  function runHealthWorker(request, data) {
5353
+ const READINESS_RETRY_MS = 250;
5374
5354
  const view = new Int32Array(data.state);
5375
5355
  let completed = false;
5376
- function finish(ok) {
5356
+ function finish(ok, rejectedAuth = false) {
5377
5357
  if (completed) return;
5378
5358
  completed = true;
5379
- Atomics.store(view, 0, ok ? 1 : 2);
5359
+ Atomics.store(view, 0, ok ? 1 : rejectedAuth ? 3 : 2);
5380
5360
  Atomics.notify(view, 0);
5381
5361
  }
5382
5362
  function probe(pathname, fallbackPath) {
@@ -5385,7 +5365,7 @@ function runHealthWorker(request, data) {
5385
5365
  finish(false);
5386
5366
  return;
5387
5367
  }
5388
- let responseReceived = false;
5368
+ let settled = false;
5389
5369
  try {
5390
5370
  const headers = {};
5391
5371
  if (data.token) headers.Authorization = `Bearer ${data.token}`;
@@ -5399,24 +5379,58 @@ function runHealthWorker(request, data) {
5399
5379
  headers
5400
5380
  },
5401
5381
  (res) => {
5402
- responseReceived = true;
5403
5382
  const statusCode = res.statusCode;
5383
+ if (statusCode === 200 && data.capture && data.captureField && res.on) {
5384
+ let body = "";
5385
+ res.setEncoding?.("utf8");
5386
+ res.on("data", (chunk) => {
5387
+ if (body.length < 65536) body += chunk ?? "";
5388
+ });
5389
+ res.on("end", () => {
5390
+ try {
5391
+ const parsed = JSON.parse(body);
5392
+ const value = typeof parsed === "object" && parsed !== null ? parsed[data.captureField] : void 0;
5393
+ if (typeof value === "string") {
5394
+ const bytes = new TextEncoder().encode(value);
5395
+ const capture = new Uint8Array(data.capture);
5396
+ new DataView(data.capture).setUint32(0, bytes.length);
5397
+ if (bytes.length <= capture.length - 4) capture.set(bytes, 4);
5398
+ }
5399
+ } catch {
5400
+ }
5401
+ settled = true;
5402
+ finish(true);
5403
+ });
5404
+ res.on("error", () => {
5405
+ if (!settled) finish(false);
5406
+ });
5407
+ return;
5408
+ }
5404
5409
  res.resume();
5410
+ settled = true;
5405
5411
  if (statusCode === 200) {
5406
5412
  finish(true);
5407
5413
  } else if (statusCode === 404 && fallbackPath) {
5408
5414
  probe(fallbackPath, null);
5415
+ } else if (statusCode === 503) {
5416
+ if (Date.now() + READINESS_RETRY_MS >= data.deadline) {
5417
+ finish(false);
5418
+ return;
5419
+ }
5420
+ setTimeout(() => probe(pathname, fallbackPath), READINESS_RETRY_MS);
5421
+ } else if (statusCode === 401 || statusCode === 403) {
5422
+ finish(false, true);
5409
5423
  } else {
5410
5424
  finish(false);
5411
5425
  }
5412
5426
  }
5413
5427
  );
5414
5428
  req.on("error", () => {
5415
- if (!responseReceived) finish(false);
5429
+ if (!settled) finish(false);
5416
5430
  });
5417
5431
  req.on("timeout", () => {
5418
5432
  req.destroy();
5419
- if (!responseReceived) finish(false);
5433
+ if (!settled) finish(false);
5420
5434
  });
5421
5435
  req.end();
5422
5436
  } catch {
@@ -5431,20 +5445,684 @@ import { workerData } from "node:worker_threads";
5431
5445
  const __name = (target) => target;
5432
5446
  (${runHealthWorker.toString()})(request, workerData);
5433
5447
  `;
5448
+
5449
+ // src/bridge-unit-discovery.ts
5450
+ import fs2 from "fs";
5451
+ import path4 from "path";
5452
+ import { expandTildePath } from "@remnic/core";
5453
+
5454
+ // src/bridge-service-units.ts
5455
+ import fs from "fs";
5456
+ import path3 from "path";
5457
+ function coercePort(value) {
5458
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value.trim()) : value;
5459
+ return typeof parsed === "number" && Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : void 0;
5460
+ }
5461
+ function foldContinuationLines(unit, joiner = " ") {
5462
+ const logical = [];
5463
+ let pending;
5464
+ for (const raw of unit.split("\n")) {
5465
+ const line = raw.replace(/\r$/, "");
5466
+ const trailing = /(\\*)$/.exec(line)?.[1]?.length ?? 0;
5467
+ const continues = trailing % 2 === 1;
5468
+ const body = continues ? line.slice(0, -1) : line;
5469
+ pending = pending === void 0 ? body : `${pending}${joiner}${joiner === "" ? body : body.trim()}`;
5470
+ if (continues) continue;
5471
+ logical.push(pending);
5472
+ pending = void 0;
5473
+ }
5474
+ if (pending !== void 0) logical.push(pending);
5475
+ return logical;
5476
+ }
5477
+ function readEffectiveDirectives(unit) {
5478
+ const env = /* @__PURE__ */ new Map();
5479
+ const envFiles = [];
5480
+ const unsetEnv = [];
5481
+ const execStart = [];
5482
+ let workingDirectory;
5483
+ for (const line of foldContinuationLines(unit)) {
5484
+ const directive = /^\s*(Environment|EnvironmentFile|UnsetEnvironment|ExecStart|WorkingDirectory)=(.*)$/.exec(
5485
+ line
5486
+ );
5487
+ if (directive === null) continue;
5488
+ const [, name, rawValue = ""] = directive;
5489
+ const value = rawValue.trim();
5490
+ if (name === "Environment") {
5491
+ if (value === "") {
5492
+ env.clear();
5493
+ continue;
5494
+ }
5495
+ for (const rawToken of value.match(/"[^"]*"|'[^']*'|\S+/g) ?? []) {
5496
+ const token = /^(["']).*\1$/.test(rawToken) ? rawToken.slice(1, -1) : rawToken;
5497
+ const split = token.indexOf("=");
5498
+ if (split <= 0) continue;
5499
+ env.set(token.slice(0, split), token.slice(split + 1));
5500
+ }
5501
+ continue;
5502
+ }
5503
+ if (name === "EnvironmentFile") {
5504
+ if (value === "") {
5505
+ envFiles.length = 0;
5506
+ continue;
5507
+ }
5508
+ for (const rawToken of value.match(/"[^"]*"|'[^']*'|\S+/g) ?? []) {
5509
+ const token = /^(["']).*\1$/.test(rawToken) ? rawToken.slice(1, -1) : rawToken;
5510
+ envFiles.push(token.startsWith("-") ? token.slice(1) : token);
5511
+ }
5512
+ continue;
5513
+ }
5514
+ if (name === "UnsetEnvironment") {
5515
+ if (value === "") {
5516
+ unsetEnv.length = 0;
5517
+ continue;
5518
+ }
5519
+ for (const rawToken of value.match(/"[^"]*"|'[^']*'|\S+/g) ?? []) {
5520
+ unsetEnv.push(/^(["']).*\1$/.test(rawToken) ? rawToken.slice(1, -1) : rawToken);
5521
+ }
5522
+ continue;
5523
+ }
5524
+ if (name === "ExecStart") {
5525
+ if (value === "") {
5526
+ execStart.length = 0;
5527
+ continue;
5528
+ }
5529
+ execStart.push(value);
5530
+ continue;
5531
+ }
5532
+ workingDirectory = value === "" ? void 0 : value;
5533
+ }
5534
+ return { env, envFiles, unsetEnv, execStart, workingDirectory };
5535
+ }
5536
+ function parseEnvironmentFile(body) {
5537
+ const parsed = /* @__PURE__ */ new Map();
5538
+ for (const line of foldContinuationLines(body, "")) {
5539
+ const trimmed = line.trim();
5540
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
5541
+ const split = trimmed.indexOf("=");
5542
+ if (split <= 0) continue;
5543
+ const key = trimmed.slice(0, split).trim();
5544
+ const raw = trimmed.slice(split + 1).trim();
5545
+ parsed.set(key, /^(["']).*\1$/.test(raw) ? raw.slice(1, -1) : raw);
5546
+ }
5547
+ return parsed;
5548
+ }
5549
+ function readUnitEnvironment(unit, scope, readFile4, listDir) {
5550
+ const directives = readEffectiveDirectives(unit);
5551
+ const merged = new Map(directives.env);
5552
+ for (const candidate of directives.envFiles) {
5553
+ const expandedFile = expandAccountRelative(candidate, scope);
5554
+ if (!scope.userScoped && (expandedFile.includes("%") || expandedFile.startsWith("~"))) continue;
5555
+ const resolved = settleUnitValue(expandedFile);
5556
+ if (!path3.isAbsolute(resolved)) continue;
5557
+ for (const match of expandEnvironmentFilePattern(resolved, listDir)) {
5558
+ const body = readFile4(match);
5559
+ if (body === void 0) continue;
5560
+ for (const [key, value] of parseEnvironmentFile(body)) merged.set(key, value);
5561
+ }
5562
+ }
5563
+ for (const name of directives.unsetEnv) {
5564
+ const split = name.indexOf("=");
5565
+ if (split <= 0) {
5566
+ merged.delete(name);
5567
+ continue;
5568
+ }
5569
+ const key = name.slice(0, split);
5570
+ if (merged.get(key) === name.slice(split + 1)) merged.delete(key);
5571
+ }
5572
+ return merged;
5573
+ }
5574
+ function expandEnvironmentFilePattern(candidate, listDir) {
5575
+ if (!/[*?[]/.test(candidate)) return [candidate];
5576
+ const directory = path3.dirname(candidate);
5577
+ const pattern = path3.basename(candidate);
5578
+ if (/[*?[]/.test(directory)) return [];
5579
+ const matcher = new RegExp(
5580
+ `^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`
5581
+ );
5582
+ return listDir(directory).filter((entry) => matcher.test(entry)).sort().map((entry) => path3.join(directory, entry));
5583
+ }
5584
+ function defaultUnitDirLister(directory) {
5585
+ try {
5586
+ return fs.readdirSync(directory);
5587
+ } catch {
5588
+ return [];
5589
+ }
5590
+ }
5591
+ function defaultUnitFileReader(candidate) {
5592
+ try {
5593
+ return fs.readFileSync(candidate, "utf8");
5594
+ } catch {
5595
+ return void 0;
5596
+ }
5597
+ }
5598
+ function readUnitEnv(unit, name, scope, readFile4 = defaultUnitFileReader, listDir = defaultUnitDirLister) {
5599
+ const systemdValue = readUnitEnvironment(unit, scope, readFile4, listDir).get(name);
5600
+ const systemd = systemdValue === void 0 ? null : [void 0, systemdValue];
5601
+ const launchdRaw = new RegExp(`<key>${name}</key>\\s*<string>([^<]*)</string>`).exec(unit);
5602
+ const launchd = launchdRaw === null ? null : [void 0, decodePlistString(launchdRaw[1] ?? "")];
5603
+ const raw = (systemd?.[1] ?? launchd?.[1])?.trim();
5604
+ if (raw === void 0) return void 0;
5605
+ if (raw === "") return "";
5606
+ const expanded = expandAccountRelative(raw, scope);
5607
+ if (!scope.userScoped && (expanded.includes("%") || expanded.startsWith("~"))) return void 0;
5608
+ return settleUnitValue(expanded);
5609
+ }
5610
+ function expandAccountRelative(value, scope) {
5611
+ const withSpecifier = expandUnitSpecifiers(value, scope);
5612
+ if (!scope.userScoped) return withSpecifier;
5613
+ if (withSpecifier === "~") return scope.homeDir;
5614
+ if (withSpecifier.startsWith("~/")) return path3.join(scope.homeDir, withSpecifier.slice(2));
5615
+ return withSpecifier;
5616
+ }
5617
+ var ESCAPED_PERCENT = "\0remnic-escaped-percent\0";
5618
+ function userRuntimeDir() {
5619
+ const getuid = globalThis.process?.getuid;
5620
+ const uid = typeof getuid === "function" ? getuid.call(globalThis.process) : void 0;
5621
+ return uid === void 0 ? "/run/user" : `/run/user/${uid}`;
5622
+ }
5623
+ function settleUnitValue(value) {
5624
+ return value.replaceAll(ESCAPED_PERCENT, "%");
5625
+ }
5626
+ function expandUnitSpecifiers(value, scope) {
5627
+ const home = scope.homeDir;
5628
+ const env = globalThis.process?.["env"];
5629
+ const xdg = (name, fallback) => {
5630
+ const configured = scope.userScoped ? env?.[name] : void 0;
5631
+ return configured !== void 0 && configured.trim() !== "" ? configured : fallback;
5632
+ };
5633
+ const directories = scope.userScoped ? {
5634
+ E: xdg("XDG_CONFIG_HOME", path3.join(home, ".config")),
5635
+ S: xdg("XDG_STATE_HOME", path3.join(home, ".local", "state")),
5636
+ C: xdg("XDG_CACHE_HOME", path3.join(home, ".cache")),
5637
+ L: path3.join(xdg("XDG_STATE_HOME", path3.join(home, ".local", "state")), "log"),
5638
+ t: xdg("XDG_RUNTIME_DIR", userRuntimeDir())
5639
+ } : { E: "/etc", S: "/var/lib", C: "/var/cache", L: "/var/log", t: "/run" };
5640
+ return value.replace(/%(.)/g, (match, specifier) => {
5641
+ if (specifier === "%") return ESCAPED_PERCENT;
5642
+ if (specifier === "h") return scope.userScoped ? home : match;
5643
+ return directories[specifier] ?? match;
5644
+ });
5645
+ }
5646
+ function decodePlistString(value) {
5647
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
5648
+ }
5649
+ function readUnitWorkingDirectory(unit) {
5650
+ const systemd = readEffectiveDirectives(unit).workingDirectory;
5651
+ const launchdRaw = /<key>WorkingDirectory<\/key>\s*<string>([^<]*)<\/string>/.exec(unit)?.[1];
5652
+ const launchd = launchdRaw === void 0 ? void 0 : decodePlistString(launchdRaw).trim();
5653
+ const raw = systemd ?? launchd;
5654
+ if (raw === void 0 || raw === "") return void 0;
5655
+ return /^(["']).*\1$/.test(raw) ? raw.slice(1, -1) : raw;
5656
+ }
5657
+ function readUnitCliOverrides(unit, scope, readFile4, listDir) {
5658
+ const tokens = [];
5659
+ const environment = readUnitEnvironment(unit, scope, readFile4, listDir);
5660
+ const substituteBraced = (token) => token.replace(
5661
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
5662
+ (match, name) => environment.get(name) ?? match
5663
+ );
5664
+ const expandToken = (token) => {
5665
+ const braced = substituteBraced(token);
5666
+ const bare = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(braced);
5667
+ const name = bare?.[1];
5668
+ if (name === void 0) return [braced];
5669
+ const value = environment.get(name);
5670
+ if (value === void 0) return [braced];
5671
+ return value.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
5672
+ };
5673
+ for (const command of readEffectiveDirectives(unit).execStart) {
5674
+ for (const token of command.match(/"[^"]*"|'[^']*'|\S+/g) ?? []) {
5675
+ tokens.push(...expandToken(token));
5676
+ }
5677
+ }
5678
+ const programArgs = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(unit);
5679
+ if (programArgs?.[1]) {
5680
+ for (const entry of programArgs[1].matchAll(/<string>([^<]*)<\/string>/g)) {
5681
+ tokens.push(decodePlistString(entry[1] ?? ""));
5682
+ }
5683
+ }
5684
+ const unquote = (value) => /^(["']).*\1$/.test(value) ? value.slice(1, -1) : value;
5685
+ const readFlag = (flag) => {
5686
+ let found;
5687
+ for (const [index, rawToken] of tokens.entries()) {
5688
+ const token = unquote(rawToken);
5689
+ if (token === flag) {
5690
+ const next = tokens[index + 1];
5691
+ if (next === void 0) continue;
5692
+ const value = unquote(next);
5693
+ if (value.startsWith("-")) continue;
5694
+ found = value;
5695
+ continue;
5696
+ }
5697
+ if (token.startsWith(`${flag}=`)) found = unquote(token.slice(flag.length + 1));
5698
+ }
5699
+ return found;
5700
+ };
5701
+ const expand = (value) => {
5702
+ if (value === void 0) return void 0;
5703
+ const expanded = expandAccountRelative(value, scope);
5704
+ if (!scope.userScoped && (expanded.includes("%") || expanded.startsWith("~"))) return void 0;
5705
+ return settleUnitValue(expanded);
5706
+ };
5707
+ const configPath = expand(readFlag("--config"));
5708
+ const resolvedConfig = configPath === void 0 ? void 0 : resolveAgainstWorkingDirectory(
5709
+ configPath,
5710
+ readUnitWorkingDirectory(unit),
5711
+ scope
5712
+ );
5713
+ const host = expand(readFlag("--host"));
5714
+ const authToken = expand(readFlag("--auth-token"));
5715
+ return {
5716
+ ...resolvedConfig !== void 0 && path3.isAbsolute(resolvedConfig) ? { configPath: resolvedConfig } : {},
5717
+ ...host === void 0 ? {} : { host },
5718
+ ...coercePort(expand(readFlag("--port"))) === void 0 ? {} : { port: coercePort(expand(readFlag("--port"))) },
5719
+ ...authToken === void 0 ? {} : { authToken }
5720
+ };
5721
+ }
5722
+ function resolveUnitEndpoint(unit, scope, readFile4 = defaultUnitFileReader, listDir = defaultUnitDirLister) {
5723
+ const cli = readUnitCliOverrides(unit, scope, readFile4, listDir);
5724
+ const envOverride = (primary, legacy) => {
5725
+ const value = readUnitEnv(unit, primary, scope, readFile4, listDir);
5726
+ if (value !== void 0) return value === "" ? void 0 : value;
5727
+ const legacyValue = readUnitEnv(unit, legacy, scope, readFile4, listDir);
5728
+ return legacyValue === "" ? void 0 : legacyValue;
5729
+ };
5730
+ const host = cli.host ?? envOverride("REMNIC_HOST", "ENGRAM_HOST");
5731
+ const port = cli.port ?? coercePort(envOverride("REMNIC_PORT", "ENGRAM_PORT"));
5732
+ const authToken = cli.authToken ?? envOverride("REMNIC_AUTH_TOKEN", "ENGRAM_AUTH_TOKEN");
5733
+ const configFromCli = cli.configPath === void 0 ? {} : { configPath: cli.configPath };
5734
+ return {
5735
+ ...resolveUnitConfigPathInner(unit, scope, readFile4, listDir),
5736
+ ...configFromCli,
5737
+ ...host === void 0 ? {} : { host },
5738
+ ...port === void 0 ? {} : { port },
5739
+ ...authToken === void 0 ? {} : { authToken }
5740
+ };
5741
+ }
5742
+ function resolveUnitConfigPathInner(unit, scope, readFile4, listDir) {
5743
+ const workingDirectory = readUnitWorkingDirectory(unit);
5744
+ for (const name of ["REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH"]) {
5745
+ const raw = readUnitEnv(unit, name, scope, readFile4, listDir);
5746
+ if (raw === "") return {};
5747
+ if (raw === void 0) continue;
5748
+ const resolved = resolveAgainstWorkingDirectory(raw, workingDirectory, scope);
5749
+ if (resolved === void 0) continue;
5750
+ return { configPath: resolved };
5751
+ }
5752
+ if (workingDirectory !== void 0) {
5753
+ for (const name of ["remnic.config.json", "engram.config.json"]) {
5754
+ const candidate = resolveAgainstWorkingDirectory(name, workingDirectory, scope);
5755
+ if (candidate !== void 0 && readFile4(candidate) !== void 0) {
5756
+ return { configPath: candidate };
5757
+ }
5758
+ }
5759
+ }
5760
+ return {};
5761
+ }
5762
+ function resolveAgainstWorkingDirectory(candidate, workingDirectory, scope) {
5763
+ if (path3.isAbsolute(candidate)) return candidate;
5764
+ if (workingDirectory === void 0) return void 0;
5765
+ if (!scope.userScoped && (workingDirectory.includes("%") || workingDirectory.startsWith("~"))) {
5766
+ return void 0;
5767
+ }
5768
+ const expanded = settleUnitValue(expandAccountRelative(workingDirectory, scope));
5769
+ if (!path3.isAbsolute(expanded)) return void 0;
5770
+ return path3.resolve(expanded, candidate);
5771
+ }
5772
+ function resolveSystemUnitSources(unitDirs, unitNames, exists = (candidate) => fs.existsSync(candidate)) {
5773
+ const sources = [];
5774
+ for (const name of unitNames) {
5775
+ const unitPath = [...unitDirs].reverse().map((dir) => path3.join(dir, name)).find((candidate) => exists(candidate));
5776
+ if (unitPath === void 0) continue;
5777
+ sources.push({ unitPath, dropInDirs: unitDirs.map((dir) => path3.join(dir, `${name}.d`)) });
5778
+ }
5779
+ return sources;
5780
+ }
5781
+
5782
+ // src/bridge-unit-discovery.ts
5783
+ function resolveHomeDir() {
5784
+ const env = globalThis.process?.["env"];
5785
+ const home = env?.["HOME"] ?? env?.["USERPROFILE"];
5786
+ return home !== void 0 && home.trim() !== "" ? expandTildePath(home) : "";
5787
+ }
5788
+ function fileExists(filePath) {
5789
+ try {
5790
+ return fs2.statSync(filePath).isFile();
5791
+ } catch {
5792
+ return false;
5793
+ }
5794
+ }
5434
5795
  var LAUNCHD_SERVICE_PATHS = [
5435
5796
  ["Library", "LaunchAgents", "ai.remnic.daemon.plist"],
5436
5797
  ["Library", "LaunchAgents", "ai.remnic.server.plist"],
5437
5798
  ["Library", "LaunchAgents", "ai.engram.daemon.plist"]
5438
5799
  ];
5439
- var SYSTEMD_SERVICE_PATHS = [
5440
- [".config", "systemd", "user", "remnic.service"],
5441
- [".config", "systemd", "user", "engram.service"]
5800
+ var SYSTEMD_UNIT_NAMES = ["remnic.service", "engram.service"];
5801
+ function dedupe(entries) {
5802
+ return [...new Set(entries)];
5803
+ }
5804
+ function systemdUserUnitDirs(homeDir) {
5805
+ const env = globalThis.process?.["env"];
5806
+ const xdgConfig = env?.["XDG_CONFIG_HOME"];
5807
+ const xdgData = env?.["XDG_DATA_HOME"];
5808
+ const underHome = (...segments) => path4.join(homeDir, ...segments);
5809
+ const xdgConfigDirs = env?.["XDG_CONFIG_DIRS"];
5810
+ const systemConfigDirs = xdgConfigDirs !== void 0 && xdgConfigDirs.trim() !== "" ? xdgConfigDirs.split(":").filter((entry) => entry.trim() !== "").map((entry) => path4.join(expandTildePath(entry), "systemd", "user")) : ["/etc/xdg/systemd/user"];
5811
+ return [
5812
+ // Distribution `share` locations rank below their `lib` counterparts.
5813
+ "/usr/share/systemd/user",
5814
+ "/usr/lib/systemd/user",
5815
+ "/usr/local/share/systemd/user",
5816
+ "/usr/local/lib/systemd/user",
5817
+ // BOTH the XDG-configured directory and the default, when they differ.
5818
+ // These variables come from the GATEWAY's environment; the daemon's user
5819
+ // manager may have been started with different ones, or none. Scanning only
5820
+ // ours would miss a unit sitting in the other location — the daemon's real
5821
+ // one. Extra directories cost a `statSync` that misses.
5822
+ ...dedupe([
5823
+ underHome(".local", "share", "systemd", "user"),
5824
+ ...xdgData !== void 0 && xdgData.trim() !== "" ? [path4.join(expandTildePath(xdgData), "systemd", "user")] : []
5825
+ ]),
5826
+ "/run/systemd/user",
5827
+ "/etc/systemd/user",
5828
+ // `XDG_CONFIG_DIRS` (default `/etc/xdg`) outranks `/etc/systemd/user` and
5829
+ // is outranked only by the user's own config directory. Listed in
5830
+ // ASCENDING precedence, so a later entry of the colon list ranks lower —
5831
+ // the reverse of how XDG reads it.
5832
+ ...systemConfigDirs.reverse(),
5833
+ ...dedupe([
5834
+ underHome(".config", "systemd", "user"),
5835
+ ...xdgConfig !== void 0 && xdgConfig.trim() !== "" ? [path4.join(expandTildePath(xdgConfig), "systemd", "user")] : []
5836
+ ])
5837
+ ];
5838
+ }
5839
+ var SYSTEMD_SYSTEM_UNIT_DIRS = [
5840
+ "/usr/lib/systemd/system",
5841
+ "/lib/systemd/system",
5842
+ "/usr/local/lib/systemd/system",
5843
+ "/run/systemd/system",
5844
+ "/etc/systemd/system"
5442
5845
  ];
5846
+ var SYSTEMD_SYSTEM_UNIT_NAMES = SYSTEMD_UNIT_NAMES;
5847
+ function readUnitDropIns(dropInDirs) {
5848
+ const byName = /* @__PURE__ */ new Map();
5849
+ for (const dropInDir of dropInDirs) {
5850
+ let entries;
5851
+ try {
5852
+ entries = fs2.readdirSync(dropInDir);
5853
+ } catch {
5854
+ continue;
5855
+ }
5856
+ for (const entry of entries.filter((name) => name.endsWith(".conf"))) {
5857
+ try {
5858
+ byName.set(entry, fs2.readFileSync(path4.join(dropInDir, entry), "utf8"));
5859
+ } catch {
5860
+ }
5861
+ }
5862
+ }
5863
+ return [...byName.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, body]) => body);
5864
+ }
5865
+ function readUnitText(source) {
5866
+ if (!fileExists(source.unitPath)) return void 0;
5867
+ let unit;
5868
+ try {
5869
+ unit = fs2.readFileSync(source.unitPath, "utf8");
5870
+ } catch {
5871
+ return void 0;
5872
+ }
5873
+ return [unit, ...readUnitDropIns(source.dropInDirs)].join("\n");
5874
+ }
5875
+ function readUnitAuthToken(source) {
5876
+ const unit = readUnitText(source);
5877
+ if (unit === void 0) return { readable: false };
5878
+ return {
5879
+ readable: true,
5880
+ token: resolveUnitEndpoint(unit, {
5881
+ userScoped: source.userScoped,
5882
+ homeDir: resolveHomeDir()
5883
+ }).authToken
5884
+ };
5885
+ }
5886
+ function readServiceEndpoints() {
5887
+ const homeDir = resolveHomeDir();
5888
+ const unitPaths = [
5889
+ ...LAUNCHD_SERVICE_PATHS.map((segments) => {
5890
+ const unitPath = path4.join(homeDir, ...segments);
5891
+ return { unitPath, dropInDirs: [`${unitPath}.d`], userScoped: true };
5892
+ }),
5893
+ // User units follow the same load-path rules as system ones: highest
5894
+ // precedence file wins, drop-ins collected from every directory.
5895
+ ...resolveSystemUnitSources(
5896
+ systemdUserUnitDirs(homeDir),
5897
+ SYSTEMD_UNIT_NAMES,
5898
+ fileExists
5899
+ ).map((source) => ({ ...source, userScoped: true })),
5900
+ // For a SYSTEM unit the base file and its overrides can live in different
5901
+ // load-path directories: a packaged unit under `/usr/lib` customized by
5902
+ // `systemctl edit`, which writes `/etc/systemd/system/<unit>.d/*.conf`.
5903
+ ...resolveSystemUnitSources(SYSTEMD_SYSTEM_UNIT_DIRS, SYSTEMD_SYSTEM_UNIT_NAMES, fileExists).map(
5904
+ (source) => ({ ...source, userScoped: false })
5905
+ )
5906
+ ];
5907
+ const endpoints = [];
5908
+ for (const source of unitPaths) {
5909
+ const { userScoped } = source;
5910
+ const unit = readUnitText(source);
5911
+ if (unit === void 0) continue;
5912
+ const resolved = resolveUnitEndpoint(unit, { userScoped, homeDir });
5913
+ if (resolved.configPath === void 0 && resolved.host === void 0 && resolved.port === void 0 && resolved.authToken === void 0) {
5914
+ continue;
5915
+ }
5916
+ const seen = endpoints.some(
5917
+ (entry) => entry.configPath === resolved.configPath && entry.host === resolved.host && entry.port === resolved.port && entry.authToken === resolved.authToken && // The UNIT is part of the identity now that credentials are re-read
5918
+ // from it per request. Canonical and legacy units can start out
5919
+ // identical while either is the active service; dropping the second
5920
+ // would pin refresh to a unit that may never change again while the
5921
+ // active one rotates its token.
5922
+ entry.authTokenUnit?.unitPath === source.unitPath
5923
+ );
5924
+ if (!seen) {
5925
+ endpoints.push({
5926
+ ...resolved,
5927
+ authTokenUnit: { ...source, dropInDirs: [...source.dropInDirs] }
5928
+ });
5929
+ }
5930
+ }
5931
+ return endpoints;
5932
+ }
5933
+ function isDaemonServiceConfigured() {
5934
+ const homeDir = resolveHomeDir();
5935
+ for (const segments of LAUNCHD_SERVICE_PATHS) {
5936
+ if (fileExists(path4.join(homeDir, ...segments))) return true;
5937
+ }
5938
+ return [...systemdUserUnitDirs(homeDir), ...SYSTEMD_SYSTEM_UNIT_DIRS].some(
5939
+ (dir) => SYSTEMD_UNIT_NAMES.some((name) => fileExists(path4.join(dir, name)))
5940
+ );
5941
+ }
5942
+
5943
+ // src/memory-read-scope.ts
5944
+ import { lstatSync, realpathSync } from "fs";
5945
+ import { realpath as fsRealpath } from "fs/promises";
5946
+ import path5 from "path";
5947
+ import { expandTildePath as expandTildePath2 } from "@remnic/core";
5948
+ function trimTrailingSeparators(value) {
5949
+ const floor = Math.max(1, path5.parse(value).root.length);
5950
+ let end = value.length;
5951
+ while (end > floor && (value[end - 1] === "/" || value[end - 1] === "\\")) end -= 1;
5952
+ return value.slice(0, end);
5953
+ }
5954
+ function defaultIsSymlink(target) {
5955
+ try {
5956
+ return lstatSync(target).isSymbolicLink();
5957
+ } catch {
5958
+ return false;
5959
+ }
5960
+ }
5961
+ var NAMESPACE_STORAGE_SEGMENT = "namespaces";
5962
+ function daemonServesCorpus(corpusRoot, daemonMemoryDir, realpath3 = realpathSync, isSymlink = defaultIsSymlink) {
5963
+ if (!corpusRoot?.trim() || !daemonMemoryDir?.trim()) return false;
5964
+ const expandedRoot = expandTildePath2(corpusRoot.trim());
5965
+ const expandedDaemon = expandTildePath2(daemonMemoryDir.trim());
5966
+ if (!path5.isAbsolute(expandedRoot) || !path5.isAbsolute(expandedDaemon)) return false;
5967
+ if (isSymlink(path5.resolve(expandedRoot)) || isSymlink(path5.resolve(expandedDaemon))) {
5968
+ return false;
5969
+ }
5970
+ let canonicalRoot;
5971
+ let canonicalDaemon;
5972
+ try {
5973
+ canonicalRoot = trimTrailingSeparators(path5.normalize(realpath3(path5.resolve(expandedRoot))));
5974
+ canonicalDaemon = trimTrailingSeparators(path5.normalize(realpath3(path5.resolve(expandedDaemon))));
5975
+ } catch {
5976
+ return false;
5977
+ }
5978
+ if (canonicalRoot === canonicalDaemon) return true;
5979
+ const relative = path5.relative(canonicalRoot, canonicalDaemon);
5980
+ if (relative.startsWith("..") || path5.isAbsolute(relative)) return false;
5981
+ const segments = relative.split(path5.sep).filter((segment) => segment.length > 0);
5982
+ return segments.length === 2 && segments[0] === NAMESPACE_STORAGE_SEGMENT;
5983
+ }
5984
+ function isSessionsMemoryPath(relativePath) {
5985
+ return /(?:^|[\\/])sessions[\\/]/i.test(relativePath);
5986
+ }
5987
+ function isMemoryArtifactPath(candidate) {
5988
+ return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(candidate);
5989
+ }
5990
+ function isContained(root, candidate) {
5991
+ const relative = path5.relative(root, candidate);
5992
+ return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
5993
+ }
5994
+ function createMemoryReadScope(options) {
5995
+ const { memoryDir, workspaceDir } = options;
5996
+ const realpath3 = options.realpath ?? fsRealpath;
5997
+ const allowedRoots = [
5998
+ memoryDir,
5999
+ // Delegate mode drops this: the daemon reports only its `memoryDir`, so a
6000
+ // gateway-local workspace it never searched or authorized must not be a
6001
+ // readable root just because the two processes share a corpus.
6002
+ workspaceDir && options.includeWorkspaceRoot !== false ? path5.join(workspaceDir, "memory") : void 0,
6003
+ ...options.additionalRoots ?? []
6004
+ ].filter((root) => typeof root === "string" && root.length > 0);
6005
+ const canonicalRootsPromise = Promise.all(
6006
+ allowedRoots.map(async (root) => {
6007
+ const resolved = path5.resolve(root);
6008
+ try {
6009
+ return path5.normalize(await realpath3(resolved));
6010
+ } catch {
6011
+ return path5.normalize(resolved);
6012
+ }
6013
+ })
6014
+ );
6015
+ const canonicalizeForRead = async (rawPath) => path5.normalize(await realpath3(path5.resolve(rawPath)));
6016
+ const normalizeWorkspacePath = (rawPath) => {
6017
+ if (!rawPath || typeof rawPath !== "string") return "memory";
6018
+ const resolved = path5.isAbsolute(rawPath) ? path5.resolve(rawPath) : path5.resolve(workspaceDir, rawPath);
6019
+ const relative = path5.relative(workspaceDir, resolved);
6020
+ return relative && !relative.startsWith("..") && !path5.isAbsolute(relative) ? relative : rawPath;
6021
+ };
6022
+ const absolutize = (rawPath) => {
6023
+ if (path5.isAbsolute(rawPath)) return path5.resolve(rawPath);
6024
+ for (const root of allowedRoots) {
6025
+ const candidate = path5.resolve(root, rawPath);
6026
+ if (isContained(root, candidate)) return candidate;
6027
+ }
6028
+ return path5.resolve(workspaceDir, rawPath);
6029
+ };
6030
+ return {
6031
+ allowedRoots,
6032
+ absolutize,
6033
+ normalizeWorkspacePath,
6034
+ relativizeToMemoryRoot(rawPath) {
6035
+ if (!rawPath || typeof rawPath !== "string") return "memory";
6036
+ const resolved = path5.isAbsolute(rawPath) ? path5.resolve(rawPath) : path5.resolve(workspaceDir, rawPath);
6037
+ for (const root of allowedRoots) {
6038
+ const relative = path5.relative(root, resolved);
6039
+ if (relative !== "" && !relative.startsWith("..") && !path5.isAbsolute(relative)) {
6040
+ return relative;
6041
+ }
6042
+ }
6043
+ return normalizeWorkspacePath(rawPath);
6044
+ },
6045
+ async resolveReadablePath(requestedPath) {
6046
+ if (typeof requestedPath !== "string" || requestedPath.length === 0) {
6047
+ throw new Error("memory read rejected (missing path)");
6048
+ }
6049
+ const candidates = path5.isAbsolute(requestedPath) ? [path5.resolve(requestedPath)] : allowedRoots.map((root) => path5.resolve(root, requestedPath));
6050
+ let canonicalPath;
6051
+ for (const candidate of candidates) {
6052
+ try {
6053
+ canonicalPath = await canonicalizeForRead(candidate);
6054
+ break;
6055
+ } catch {
6056
+ }
6057
+ }
6058
+ if (canonicalPath === void 0) {
6059
+ throw new Error(`memory read rejected (path unresolvable): ${requestedPath}`);
6060
+ }
6061
+ const canonicalRoots = await canonicalRootsPromise;
6062
+ const containingRoot = canonicalRoots.find((root) => isContained(root, canonicalPath));
6063
+ if (containingRoot === void 0) {
6064
+ throw new Error(`memory read outside allowed roots: ${requestedPath}`);
6065
+ }
6066
+ if (!canonicalPath.toLowerCase().endsWith(".md")) {
6067
+ throw new Error(`memory read restricted to .md files: ${requestedPath}`);
6068
+ }
6069
+ const rootRelative = path5.relative(containingRoot, canonicalPath);
6070
+ if (rootRelative.startsWith("..") || path5.isAbsolute(rootRelative)) {
6071
+ throw new Error(`memory read outside allowed roots: ${requestedPath}`);
6072
+ }
6073
+ const rawIsArtifact = !path5.isAbsolute(requestedPath) && isMemoryArtifactPath(requestedPath);
6074
+ if (isMemoryArtifactPath(rootRelative) || rawIsArtifact) {
6075
+ throw new Error(`memory read excluded (artifact path): ${requestedPath}`);
6076
+ }
6077
+ return canonicalPath;
6078
+ }
6079
+ };
6080
+ }
6081
+
6082
+ // src/bridge.ts
6083
+ function daemonUrl(target, pathname) {
6084
+ const host = target.host.includes(":") && !target.host.startsWith("[") ? `[${target.host}]` : target.host;
6085
+ return `http://${host}:${target.port}${pathname}`;
6086
+ }
6087
+ function daemonAuthHeaders(target) {
6088
+ const auth = target.resolveAuthToken();
6089
+ return auth.token ? { Authorization: `Bearer ${auth.token}` } : {};
6090
+ }
6091
+ var DEFAULT_HOST = "127.0.0.1";
6092
+ var DEFAULT_PORT = 4318;
6093
+ var LIVENESS_PATH = "/engram/v1/live";
6094
+ var LEGACY_HEALTH_PATH = "/engram/v1/health";
6095
+ var DEFAULT_DAEMON_HEALTH_TIMEOUT_MS = 1e4;
6096
+ function assertProbeBudget(timeoutMs) {
6097
+ if (timeoutMs === void 0) return DEFAULT_DAEMON_HEALTH_TIMEOUT_MS;
6098
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_BRIDGE_HEALTH_TIMEOUT_MS) {
6099
+ throw new Error(
6100
+ `timeoutMs must be an integer in [1, ${MAX_BRIDGE_HEALTH_TIMEOUT_MS}]; got ${String(timeoutMs)}`
6101
+ );
6102
+ }
6103
+ return timeoutMs;
6104
+ }
6105
+ var MAX_BRIDGE_HEALTH_TIMEOUT_MS = 12e4;
6106
+ function parseBridgeHealthTimeoutMs(value) {
6107
+ if (value === void 0) return DEFAULT_DAEMON_HEALTH_TIMEOUT_MS;
6108
+ const parsed = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
6109
+ if (typeof parsed !== "number" || !Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > MAX_BRIDGE_HEALTH_TIMEOUT_MS) {
6110
+ throw new Error(
6111
+ `bridgeHealthTimeoutMs must be an integer in [1, ${MAX_BRIDGE_HEALTH_TIMEOUT_MS}]; got ${String(value)}`
6112
+ );
6113
+ }
6114
+ return parsed;
6115
+ }
6116
+ function parseOpenClawBridgeConfig(config) {
6117
+ return {
6118
+ healthTimeoutMs: parseBridgeHealthTimeoutMs(config.bridgeHealthTimeoutMs)
6119
+ };
6120
+ }
5443
6121
  function readEnv(name) {
5444
6122
  const env = globalThis.process?.["env"];
5445
6123
  return env?.[name];
5446
6124
  }
5447
- function resolveHomeDir() {
6125
+ function resolveHomeDir2() {
5448
6126
  return readEnv("HOME") ?? readEnv("USERPROFILE") ?? "~";
5449
6127
  }
5450
6128
  function readCompatEnv(primary, legacy) {
@@ -5453,27 +6131,27 @@ function readCompatEnv(primary, legacy) {
5453
6131
  function configPathCandidates() {
5454
6132
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
5455
6133
  return [
5456
- ...envPath ? [path3.resolve(expandTildePath(envPath))] : [],
5457
- path3.join(resolveHomeDir(), ".config", "remnic", "config.json"),
5458
- path3.join(resolveHomeDir(), ".config", "engram", "config.json"),
5459
- path3.join(process.cwd(), "remnic.config.json"),
5460
- path3.join(process.cwd(), "engram.config.json")
6134
+ ...envPath ? [path6.resolve(expandTildePath3(envPath))] : [],
6135
+ path6.join(process.cwd(), "remnic.config.json"),
6136
+ path6.join(process.cwd(), "engram.config.json"),
6137
+ path6.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
6138
+ path6.join(resolveHomeDir2(), ".config", "engram", "config.json")
5461
6139
  ];
5462
6140
  }
5463
- function fileExists(filePath) {
6141
+ function fileExists2(filePath) {
5464
6142
  try {
5465
- return fs.statSync(filePath).isFile();
6143
+ return fs3.statSync(filePath).isFile();
5466
6144
  } catch {
5467
6145
  return false;
5468
6146
  }
5469
6147
  }
5470
6148
  function isDaemonRunning() {
5471
6149
  for (const pidFile of [
5472
- path3.join(resolveHomeDir(), ".remnic", "server.pid"),
5473
- path3.join(resolveHomeDir(), ".engram", "server.pid")
6150
+ path6.join(resolveHomeDir2(), ".remnic", "server.pid"),
6151
+ path6.join(resolveHomeDir2(), ".engram", "server.pid")
5474
6152
  ]) {
5475
6153
  try {
5476
- const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
6154
+ const pid = parseInt(fs3.readFileSync(pidFile, "utf8").trim(), 10);
5477
6155
  process.kill(pid, 0);
5478
6156
  return true;
5479
6157
  } catch {
@@ -5481,12 +6159,33 @@ function isDaemonRunning() {
5481
6159
  }
5482
6160
  return false;
5483
6161
  }
5484
- function isDaemonServiceConfigured() {
5485
- const homeDir = resolveHomeDir();
5486
- for (const segments of [...LAUNCHD_SERVICE_PATHS, ...SYSTEMD_SERVICE_PATHS]) {
5487
- if (fileExists(path3.join(homeDir, ...segments))) return true;
6162
+ function isLoopbackDaemonHost(host) {
6163
+ const normalized = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
6164
+ if (normalized === "localhost") return true;
6165
+ if (loopbackForWildcardBind(normalized) !== void 0) return true;
6166
+ const ipv6 = canonicalIPv6(normalized);
6167
+ if (ipv6 !== void 0) {
6168
+ if (ipv6 === "::1") return true;
6169
+ const mappedHex = /^::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}$/.exec(ipv6);
6170
+ if (mappedHex !== null) return Number.parseInt(mappedHex[1], 16) >> 8 === 127;
6171
+ const mappedDotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(ipv6);
6172
+ return mappedDotted !== null && isIPv4(mappedDotted[1]) && mappedDotted[1].split(".")[0] === "127";
6173
+ }
6174
+ return isIPv4(normalized) && normalized.split(".")[0] === "127";
6175
+ }
6176
+ function canonicalIPv6(value) {
6177
+ if (!isIPv6(value)) return void 0;
6178
+ try {
6179
+ return new URL(`http://[${value}]`).hostname.replace(/^\[/, "").replace(/\]$/, "");
6180
+ } catch {
6181
+ return value;
5488
6182
  }
5489
- return false;
6183
+ }
6184
+ function loopbackForWildcardBind(host) {
6185
+ const normalized = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
6186
+ if (normalized === "0.0.0.0") return DEFAULT_HOST;
6187
+ if (canonicalIPv6(normalized) === "::") return "::1";
6188
+ return void 0;
5490
6189
  }
5491
6190
  function normalizeDaemonHost(value) {
5492
6191
  const match = value.trim().match(/^\[(.+)\]$/);
@@ -5496,130 +6195,362 @@ function coerceDaemonPort(value) {
5496
6195
  const parsed = typeof value === "string" && value.trim() !== "" ? Number(value.trim()) : value;
5497
6196
  return typeof parsed === "number" && Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : void 0;
5498
6197
  }
5499
- function checkDaemonHealthSync(host, port, timeoutMs = DEFAULT_DAEMON_HEALTH_TIMEOUT_MS) {
5500
- if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) return false;
6198
+ var DAEMON_CAPTURE_BYTES = 1024;
6199
+ function probeDaemonSync(options) {
6200
+ const { host, port, timeoutMs } = options;
6201
+ if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) return { ok: false };
5501
6202
  const deadline = Date.now() + timeoutMs;
5502
6203
  let worker;
5503
6204
  try {
5504
6205
  const state = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
5505
6206
  const view = new Int32Array(state);
6207
+ const capture = options.captureField ? new SharedArrayBuffer(DAEMON_CAPTURE_BYTES) : void 0;
5506
6208
  const workerUrl = new URL(`data:text/javascript,${encodeURIComponent(HEALTH_WORKER_SOURCE)}`);
5507
6209
  const workerOptions = {
5508
6210
  type: "module",
5509
6211
  workerData: {
5510
6212
  host,
5511
6213
  port,
5512
- path: LIVENESS_PATH,
5513
- fallbackPath: LEGACY_HEALTH_PATH,
5514
- token: loadDaemonAuth().token,
6214
+ path: options.path,
6215
+ fallbackPath: options.fallbackPath,
6216
+ token: options.authToken ?? loadDaemonAuth(options.configPath).token,
5515
6217
  deadline,
5516
- state
6218
+ state,
6219
+ ...capture ? { capture, captureField: options.captureField } : {}
5517
6220
  }
5518
6221
  };
5519
6222
  worker = new Worker(workerUrl, workerOptions);
5520
6223
  Atomics.wait(view, 0, 0, Math.max(0, deadline - Date.now()));
5521
6224
  const status = Atomics.load(view, 0);
5522
6225
  if (status === 0) void worker.terminate();
5523
- return status === 1;
6226
+ if (status !== 1) return { ok: false, ...status === 3 ? { rejectedAuth: true } : {} };
6227
+ if (!capture) return { ok: true };
6228
+ const length = new DataView(capture).getUint32(0);
6229
+ if (length === 0) return { ok: true };
6230
+ if (length > capture.byteLength - 4) return { ok: true };
6231
+ return { ok: true, captured: new TextDecoder().decode(new Uint8Array(capture, 4, length)) };
5524
6232
  } catch {
5525
6233
  if (worker) void worker.terminate();
5526
- return false;
6234
+ return { ok: false };
5527
6235
  }
5528
6236
  }
5529
- function shouldProbeDaemonHealth(host) {
5530
- const normalized = host.trim().toLowerCase();
5531
- return normalized === DEFAULT_HOST || normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || isDaemonServiceConfigured();
5532
- }
5533
- function readDaemonHost() {
5534
- const envHost = readCompatEnv("REMNIC_HOST", "ENGRAM_HOST");
5535
- if (envHost !== void 0 && envHost.trim() !== "") return normalizeDaemonHost(envHost);
5536
- for (const p of configPathCandidates()) {
5537
- if (!fs.existsSync(p)) continue;
5538
- try {
5539
- const raw = JSON.parse(fs.readFileSync(p, "utf8"));
5540
- const configHost = raw.server?.host;
5541
- if (typeof configHost === "string" && configHost.trim() !== "") {
5542
- return normalizeDaemonHost(configHost);
5543
- }
5544
- } catch {
6237
+ function checkDaemonHealthSync(host, port, timeoutMs = DEFAULT_DAEMON_HEALTH_TIMEOUT_MS) {
6238
+ assertProbeBudget(timeoutMs);
6239
+ return probeDaemonSync({
6240
+ host,
6241
+ port,
6242
+ timeoutMs,
6243
+ path: LIVENESS_PATH,
6244
+ fallbackPath: LEGACY_HEALTH_PATH
6245
+ }).ok;
6246
+ }
6247
+ function readDaemonMemoryDirSync(host, port, timeoutMs = DEFAULT_DAEMON_HEALTH_TIMEOUT_MS, configPath, authToken) {
6248
+ assertProbeBudget(timeoutMs);
6249
+ const probe = probeDaemonSync({
6250
+ host,
6251
+ port,
6252
+ timeoutMs,
6253
+ path: LEGACY_HEALTH_PATH,
6254
+ fallbackPath: null,
6255
+ captureField: "memoryDir",
6256
+ configPath,
6257
+ authToken
6258
+ });
6259
+ return {
6260
+ healthy: probe.ok,
6261
+ memoryDir: probe.captured,
6262
+ // Present only when true, so the result shape is unchanged for callers
6263
+ // that compare it structurally.
6264
+ ...probe.rejectedAuth === true ? { rejectedAuth: true } : {}
6265
+ };
6266
+ }
6267
+ function shouldProbeDaemonHealth(host) {
6268
+ return isLoopbackDaemonHost(host) || isDaemonServiceConfigured();
6269
+ }
6270
+ function readDaemonHost() {
6271
+ const resolved = readConfiguredDaemonHost();
6272
+ return loopbackForWildcardBind(resolved) ?? resolved;
6273
+ }
6274
+ function readDaemonServerConfig() {
6275
+ for (const candidate of configPathCandidates()) {
6276
+ const server = readServerBlock(candidate);
6277
+ if (server !== void 0) return server;
6278
+ }
6279
+ return {};
6280
+ }
6281
+ function readServerBlock(candidate) {
6282
+ if (!fileExists2(candidate)) return void 0;
6283
+ let raw;
6284
+ try {
6285
+ raw = JSON.parse(fs3.readFileSync(candidate, "utf8"));
6286
+ } catch {
6287
+ return void 0;
6288
+ }
6289
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
6290
+ const server = raw.server;
6291
+ if (server === void 0) return {};
6292
+ if (typeof server !== "object" || server === null || Array.isArray(server)) return void 0;
6293
+ const { host, port } = server;
6294
+ const parsedPort = coerceDaemonPort(port);
6295
+ const { authToken } = server;
6296
+ return {
6297
+ ...typeof host === "string" && host.trim() !== "" ? { host } : {},
6298
+ ...parsedPort === void 0 ? {} : { port: parsedPort },
6299
+ ...typeof authToken === "string" && authToken.length > 0 ? { authToken } : {}
6300
+ };
6301
+ }
6302
+ function daemonEndpointCandidates() {
6303
+ const envHost = readCompatEnv("REMNIC_HOST", "ENGRAM_HOST");
6304
+ const envPort = coerceDaemonPort(readCompatEnv("REMNIC_PORT", "ENGRAM_PORT"));
6305
+ const candidates = [];
6306
+ const add = (host, port, configPath, authTokenOverride, authTokenUnit) => {
6307
+ const resolvedHost = normalizeDaemonHost(
6308
+ envHost !== void 0 && envHost.trim() !== "" ? envHost : host ?? DEFAULT_HOST
6309
+ );
6310
+ const dialHost = loopbackForWildcardBind(resolvedHost) ?? resolvedHost;
6311
+ const dialPort = envPort ?? port ?? DEFAULT_PORT;
6312
+ const token = authTokenOverride ?? loadDaemonAuth(configPath).token;
6313
+ const configToken = configPath === void 0 ? void 0 : readServerBlock(configPath)?.authToken;
6314
+ const fallbackToken = configToken !== void 0 && configToken !== token ? configToken : void 0;
6315
+ if (candidates.some(
6316
+ (c) => c.host === dialHost && c.port === dialPort && c.token === token && // The BOUND credential is part of the identity too: when a gateway
6317
+ // token wins for both, two configs on one endpoint resolve the same
6318
+ // primary token but carry different fallbacks, and dropping the
6319
+ // second would leave the daemon's real credential untried.
6320
+ c.fallbackToken === fallbackToken && // So is the UNIT the credential is re-read from per request: two
6321
+ // units can agree today and diverge on the next rotation.
6322
+ c.authTokenUnit?.unitPath === authTokenUnit?.unitPath && // And so is the CONFIG, for the same reason: `daemonConfigPath` is
6323
+ // re-read per request, so collapsing two configs that agree today
6324
+ // would keep sending the retained one's token after the other
6325
+ // rotates.
6326
+ c.configPath === configPath
6327
+ )) {
6328
+ return;
5545
6329
  }
6330
+ candidates.push({
6331
+ host: dialHost,
6332
+ port: dialPort,
6333
+ configPath,
6334
+ token,
6335
+ ...authTokenOverride === void 0 ? {} : { authTokenOverride },
6336
+ ...authTokenUnit === void 0 ? {} : { authTokenUnit },
6337
+ ...fallbackToken === void 0 ? {} : { fallbackToken }
6338
+ });
6339
+ };
6340
+ if (envHost !== void 0 && envHost.trim() !== "" || envPort !== void 0) {
6341
+ add(void 0, void 0);
6342
+ }
6343
+ const configOrder = configPathCandidates();
6344
+ const envConfigPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH") ? configOrder[0] : void 0;
6345
+ const addConfigCandidate = (candidate) => {
6346
+ const server = readServerBlock(candidate);
6347
+ if (server !== void 0) add(server.host, server.port, candidate);
6348
+ };
6349
+ if (envConfigPath !== void 0) addConfigCandidate(envConfigPath);
6350
+ for (const unit of readServiceEndpoints()) {
6351
+ const server = unit.configPath === void 0 ? {} : readServerBlock(unit.configPath) ?? {};
6352
+ add(
6353
+ unit.host ?? server.host,
6354
+ unit.port ?? server.port,
6355
+ unit.configPath,
6356
+ unit.authToken,
6357
+ unit.authTokenUnit
6358
+ );
6359
+ }
6360
+ for (const candidate of configOrder) {
6361
+ if (candidate === envConfigPath) continue;
6362
+ addConfigCandidate(candidate);
5546
6363
  }
5547
- return DEFAULT_HOST;
6364
+ add(void 0, void 0);
6365
+ return candidates;
6366
+ }
6367
+ function readConfiguredDaemonHost() {
6368
+ const envHost = readCompatEnv("REMNIC_HOST", "ENGRAM_HOST");
6369
+ if (envHost !== void 0 && envHost.trim() !== "") return normalizeDaemonHost(envHost);
6370
+ const configHost = readDaemonServerConfig().host;
6371
+ return configHost === void 0 ? DEFAULT_HOST : normalizeDaemonHost(configHost);
5548
6372
  }
5549
6373
  function readDaemonPort() {
5550
6374
  const envPort = coerceDaemonPort(readCompatEnv("REMNIC_PORT", "ENGRAM_PORT"));
5551
6375
  if (envPort !== void 0) return envPort;
5552
- for (const p of configPathCandidates()) {
5553
- if (!fs.existsSync(p)) continue;
5554
- try {
5555
- const raw = JSON.parse(fs.readFileSync(p, "utf8"));
5556
- const configPort = coerceDaemonPort(raw.server?.port);
5557
- if (configPort !== void 0) return configPort;
5558
- } catch {
5559
- }
5560
- }
5561
- return DEFAULT_PORT;
6376
+ return readDaemonServerConfig().port ?? DEFAULT_PORT;
5562
6377
  }
5563
- function detectBridgeMode() {
5564
- const envMode = readCompatEnv("REMNIC_BRIDGE_MODE", "ENGRAM_BRIDGE_MODE")?.toLowerCase();
5565
- if (envMode === "delegate") {
5566
- return {
5567
- mode: "delegate",
5568
- daemonHost: readDaemonHost(),
5569
- daemonPort: readDaemonPort()
5570
- };
5571
- }
5572
- if (envMode === "embedded") {
5573
- return {
5574
- mode: "embedded",
5575
- daemonHost: DEFAULT_HOST,
5576
- daemonPort: readDaemonPort()
5577
- };
5578
- }
5579
- const daemonHost = readDaemonHost();
5580
- const daemonPort = readDaemonPort();
5581
- const hasDaemonPidHint = isDaemonRunning();
5582
- if ((hasDaemonPidHint || shouldProbeDaemonHealth(daemonHost)) && checkDaemonHealthSync(daemonHost, daemonPort)) {
6378
+ function detectDaemonBridgeMode(options) {
6379
+ const endpoints = daemonEndpointCandidates();
6380
+ const primary = endpoints[0] ?? { host: readDaemonHost(), port: readDaemonPort(), token: "" };
6381
+ const embedded = {
6382
+ mode: "embedded",
6383
+ daemonHost: primary.host,
6384
+ daemonPort: primary.port,
6385
+ ...primary.configPath === void 0 ? {} : { daemonConfigPath: primary.configPath },
6386
+ ...primary.authTokenOverride === void 0 ? {} : { daemonAuthTokenOverride: primary.authTokenOverride },
6387
+ ...primary.authTokenUnit === void 0 ? {} : { daemonAuthUnit: primary.authTokenUnit }
6388
+ };
6389
+ if (options.memoryDir.trim() === "") {
6390
+ throw new Error("detectDaemonBridgeMode requires a non-empty memoryDir to verify the daemon corpus");
6391
+ }
6392
+ const totalTimeoutMs = assertProbeBudget(options.timeoutMs);
6393
+ const deadline = Date.now() + totalTimeoutMs;
6394
+ const probeable = [];
6395
+ for (const candidate of endpoints) {
6396
+ if (!isLoopbackDaemonHost(candidate.host)) {
6397
+ options.onSkip?.(
6398
+ `daemon endpoint ${candidate.host}:${candidate.port} is not loopback; auto only delegates to a same-host daemon`
6399
+ );
6400
+ continue;
6401
+ }
6402
+ if (!isDaemonRunning() && !shouldProbeDaemonHealth(candidate.host)) {
6403
+ options.onSkip?.("no daemon PID, service unit, or local endpoint to probe");
6404
+ continue;
6405
+ }
6406
+ probeable.push(candidate);
6407
+ }
6408
+ let probed = 0;
6409
+ for (const {
6410
+ host: daemonHost,
6411
+ port: daemonPort,
6412
+ configPath,
6413
+ authTokenOverride,
6414
+ authTokenUnit,
6415
+ fallbackToken
6416
+ } of probeable) {
6417
+ const remainingMs = deadline - Date.now();
6418
+ if (remainingMs <= 0) {
6419
+ options.onSkip?.(
6420
+ `preflight budget of ${totalTimeoutMs}ms is spent; ${daemonHost}:${daemonPort} was not probed`
6421
+ );
6422
+ break;
6423
+ }
6424
+ const perCandidateMs = Math.max(
6425
+ 1,
6426
+ Math.ceil(remainingMs / Math.max(1, probeable.length - probed))
6427
+ );
6428
+ probed += 1;
6429
+ const candidateDeadline = Date.now() + Math.min(remainingMs, perCandidateMs);
6430
+ const firstAttemptMs = candidateDeadline - Date.now();
6431
+ if (firstAttemptMs <= 0) {
6432
+ options.onSkip?.(
6433
+ `preflight budget of ${totalTimeoutMs}ms is spent; ${daemonHost}:${daemonPort} was not probed`
6434
+ );
6435
+ continue;
6436
+ }
6437
+ let usedToken = authTokenOverride;
6438
+ let health = readDaemonMemoryDirSync(
6439
+ daemonHost,
6440
+ daemonPort,
6441
+ firstAttemptMs,
6442
+ configPath,
6443
+ usedToken
6444
+ );
6445
+ if (health.rejectedAuth === true && fallbackToken !== void 0) {
6446
+ const retryMs = candidateDeadline - Date.now();
6447
+ if (retryMs > 0) {
6448
+ usedToken = fallbackToken;
6449
+ health = readDaemonMemoryDirSync(daemonHost, daemonPort, retryMs, configPath, usedToken);
6450
+ }
6451
+ }
6452
+ if (!health.healthy) {
6453
+ options.onSkip?.(`no healthy daemon at ${daemonHost}:${daemonPort}`);
6454
+ continue;
6455
+ }
6456
+ if (health.memoryDir === void 0) {
6457
+ options.onSkip?.(
6458
+ `daemon at ${daemonHost}:${daemonPort} did not report a memoryDir, so its corpus cannot be confirmed`
6459
+ );
6460
+ continue;
6461
+ }
6462
+ if (!daemonServesCorpus(options.memoryDir, health.memoryDir)) {
6463
+ options.onSkip?.(
6464
+ `daemon at ${daemonHost}:${daemonPort} serves a different memoryDir than this plugin`
6465
+ );
6466
+ continue;
6467
+ }
5583
6468
  return {
5584
6469
  mode: "delegate",
5585
6470
  daemonHost,
5586
- daemonPort
6471
+ daemonPort,
6472
+ healthVerified: true,
6473
+ ...configPath === void 0 ? {} : { daemonConfigPath: configPath },
6474
+ // A unit-supplied credential has no other source, so it is carried by
6475
+ // value; a config-supplied one is re-read from its file per request.
6476
+ ...usedToken !== void 0 && usedToken === authTokenOverride ? {
6477
+ daemonAuthTokenOverride: usedToken,
6478
+ // The unit rides along so the credential can be re-read per
6479
+ // request; the frozen value stays as the fallback for a unit that
6480
+ // later becomes unreadable.
6481
+ ...authTokenUnit === void 0 ? {} : { daemonAuthUnit: authTokenUnit }
6482
+ } : {},
6483
+ ...usedToken !== void 0 && usedToken === fallbackToken ? { daemonAuthPrefersConfig: true } : {}
5587
6484
  };
5588
6485
  }
5589
- return {
5590
- mode: "embedded",
5591
- daemonHost: DEFAULT_HOST,
5592
- daemonPort
5593
- };
6486
+ return embedded;
5594
6487
  }
5595
- function resolveBridgeMode(configBridgeMode) {
6488
+ function resolveRequestedBridgeMode(configBridgeMode) {
5596
6489
  const envMode = readCompatEnv("REMNIC_BRIDGE_MODE", "ENGRAM_BRIDGE_MODE")?.toLowerCase();
5597
- let mode;
5598
- if (envMode === "delegate" || envMode === "embedded") {
5599
- mode = envMode;
5600
- } else if (envMode !== void 0 && envMode !== "") {
5601
- throw new Error(
5602
- `Invalid REMNIC_BRIDGE_MODE env override: ${envMode} (expected "embedded" or "delegate")`
5603
- );
5604
- } else if (configBridgeMode === void 0 || configBridgeMode === "" || configBridgeMode === "embedded") {
5605
- mode = "embedded";
5606
- } else if (configBridgeMode === "delegate") {
5607
- mode = "delegate";
5608
- } else {
5609
- throw new Error(
5610
- `Invalid bridgeMode: ${String(configBridgeMode)} (expected "embedded" or "delegate")`
5611
- );
6490
+ const isRequest = (value) => value === "embedded" || value === "delegate" || value === "auto";
6491
+ if (envMode !== void 0 && envMode !== "") {
6492
+ if (!isRequest(envMode)) {
6493
+ throw new Error(
6494
+ `Invalid REMNIC_BRIDGE_MODE env override: ${envMode} (expected "embedded", "delegate", or "auto")`
6495
+ );
6496
+ }
6497
+ return envMode;
5612
6498
  }
6499
+ if (configBridgeMode === void 0 || configBridgeMode === "") return "embedded";
6500
+ if (isRequest(configBridgeMode)) return configBridgeMode;
6501
+ throw new Error(
6502
+ `Invalid bridgeMode: ${String(configBridgeMode)} (expected "embedded", "delegate", or "auto")`
6503
+ );
6504
+ }
6505
+ function detectBridgeMode(options = {}) {
6506
+ const requested = resolveRequestedBridgeMode("");
6507
+ if (requested === "auto" && !options.memoryDir?.trim()) {
6508
+ return { mode: "embedded", daemonHost: readDaemonHost(), daemonPort: readDaemonPort() };
6509
+ }
6510
+ return resolveBridgeMode("", options);
6511
+ }
6512
+ function requestedDelegate(configBridgeMode) {
6513
+ try {
6514
+ return resolveRequestedBridgeMode(configBridgeMode) !== "embedded";
6515
+ } catch {
6516
+ return true;
6517
+ }
6518
+ }
6519
+ function resolveBridgeMode(configBridgeMode, options = {}) {
6520
+ const requested = resolveRequestedBridgeMode(configBridgeMode);
6521
+ assertProbeBudget(options.timeoutMs);
6522
+ if (requested === "auto") {
6523
+ const memoryDir = options.memoryDir ?? "";
6524
+ if (!memoryDir.trim()) {
6525
+ throw new Error('bridgeMode "auto" requires a configured memoryDir to verify the daemon corpus');
6526
+ }
6527
+ return detectDaemonBridgeMode({
6528
+ memoryDir,
6529
+ timeoutMs: options.timeoutMs,
6530
+ onSkip: options.onSkip
6531
+ });
6532
+ }
6533
+ const selectedConfig = selectedDaemonConfigPath();
5613
6534
  return {
5614
- mode,
6535
+ mode: requested,
5615
6536
  daemonHost: readDaemonHost(),
5616
- daemonPort: readDaemonPort()
6537
+ daemonPort: readDaemonPort(),
6538
+ ...selectedConfig === void 0 ? {} : { daemonConfigPath: selectedConfig }
5617
6539
  };
5618
6540
  }
6541
+ function readDaemonConfigAuthToken(configPath) {
6542
+ return readServerBlock(configPath)?.authToken;
6543
+ }
6544
+ function selectedDaemonConfigPath() {
6545
+ for (const candidate of configPathCandidates()) {
6546
+ if (readServerBlock(candidate) !== void 0) return candidate;
6547
+ }
6548
+ return void 0;
6549
+ }
5619
6550
  function isOpenClawTokenEntry(value) {
5620
6551
  return value !== null && typeof value === "object" && "connector" in value && value.connector === "openclaw" && "token" in value && typeof value.token === "string";
5621
6552
  }
5622
- function loadDaemonAuth() {
6553
+ function loadDaemonAuth(configPath) {
5623
6554
  const environmentTokens = [
5624
6555
  ["OPENCLAW_REMNIC_ACCESS_TOKEN", readEnv("OPENCLAW_REMNIC_ACCESS_TOKEN")],
5625
6556
  ["REMNIC_AUTH_TOKEN", readEnv("REMNIC_AUTH_TOKEN")],
@@ -5630,13 +6561,13 @@ function loadDaemonAuth() {
5630
6561
  if (token) return { token, source };
5631
6562
  }
5632
6563
  const tokenStores = [
5633
- { path: path3.join(resolveHomeDir(), ".remnic", "tokens.json"), source: "remnic token store" },
5634
- { path: path3.join(resolveHomeDir(), ".engram", "tokens.json"), source: "engram token store" }
6564
+ { path: path6.join(resolveHomeDir2(), ".remnic", "tokens.json"), source: "remnic token store" },
6565
+ { path: path6.join(resolveHomeDir2(), ".engram", "tokens.json"), source: "engram token store" }
5635
6566
  ];
5636
6567
  for (const tokenStore of tokenStores) {
5637
- if (!fs.existsSync(tokenStore.path)) continue;
6568
+ if (!fs3.existsSync(tokenStore.path)) continue;
5638
6569
  try {
5639
- const store = JSON.parse(fs.readFileSync(tokenStore.path, "utf8"));
6570
+ const store = JSON.parse(fs3.readFileSync(tokenStore.path, "utf8"));
5640
6571
  const tokens = Array.isArray(store.tokens) ? store.tokens : [];
5641
6572
  const openClawToken = tokens.find(isOpenClawTokenEntry)?.token;
5642
6573
  if (typeof openClawToken === "string" && openClawToken.length > 0) {
@@ -5649,17 +6580,19 @@ function loadDaemonAuth() {
5649
6580
  continue;
5650
6581
  }
5651
6582
  }
5652
- try {
5653
- for (const configPath of configPathCandidates()) {
5654
- if (!fs.existsSync(configPath)) continue;
5655
- const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
5656
- const token = raw.server?.authToken;
6583
+ for (const candidate of configPath === void 0 ? configPathCandidates() : [configPath]) {
6584
+ if (!fileExists2(candidate)) continue;
6585
+ try {
6586
+ const raw = JSON.parse(fs3.readFileSync(candidate, "utf8"));
6587
+ const server = raw?.server;
6588
+ const token = server?.authToken;
5657
6589
  if (typeof token === "string" && token.length > 0) {
5658
6590
  return { token, source: "daemon configuration" };
5659
6591
  }
6592
+ } catch {
6593
+ continue;
5660
6594
  }
5661
- } catch {
5662
- return { token: "", source: "no configured token" };
6595
+ if (configPath !== void 0) break;
5663
6596
  }
5664
6597
  return { token: "", source: "no configured token" };
5665
6598
  }
@@ -5706,6 +6639,289 @@ async function checkDaemonHealth(host, port, timeoutMs = DEFAULT_DAEMON_HEALTH_T
5706
6639
  }
5707
6640
  }
5708
6641
 
6642
+ // src/delegate-authorization.ts
6643
+ var DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS = [
6644
+ "recall",
6645
+ "observe",
6646
+ "lcm_compaction_flush",
6647
+ // The daemon-backed memory-slot capability searches through
6648
+ // /engram/v1/memories/search, which enforces its own `memory_search`
6649
+ // operation. Omitting it here would let the preflight report a
6650
+ // least-privilege token authorized while every capability search 403s.
6651
+ "memory_search"
6652
+ ];
6653
+ var daemonAuthFailureLogKeys = /* @__PURE__ */ new Set();
6654
+ function reportDaemonAuthorizationFailure(serviceId, pathname, status, tokenSource) {
6655
+ const key = `${serviceId}:${pathname}:${status}:${tokenSource}`;
6656
+ if (daemonAuthFailureLogKeys.has(key)) return;
6657
+ daemonAuthFailureLogKeys.add(key);
6658
+ log2.error(
6659
+ `delegate ${pathname} authorization failed (${status}; token source: ${tokenSource})`
6660
+ );
6661
+ }
6662
+ var AUTHORIZATION_PROBE_TIMEOUT_MS = 2e3;
6663
+ async function probeDelegateAuthorization(target, namespace = "", operations = DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS, timeoutMs) {
6664
+ const auth = target.resolveAuthToken();
6665
+ const headers = auth.token ? { Authorization: `Bearer ${auth.token}` } : void 0;
6666
+ const query = new URLSearchParams();
6667
+ for (const operation of operations) query.append("op", operation);
6668
+ query.set("namespace", namespace);
6669
+ try {
6670
+ const response = await fetch(daemonUrl(target, `/engram/v1/authorization?${query}`), {
6671
+ headers,
6672
+ signal: AbortSignal.timeout(
6673
+ timeoutMs === void 0 ? AUTHORIZATION_PROBE_TIMEOUT_MS : Math.min(AUTHORIZATION_PROBE_TIMEOUT_MS, timeoutMs)
6674
+ )
6675
+ });
6676
+ await response.body?.cancel();
6677
+ if (response.status === 200) {
6678
+ return { state: "authorized", tokenSource: auth.source };
6679
+ }
6680
+ if (response.status === 401 || response.status === 403) {
6681
+ return { state: "unauthorized", status: response.status, tokenSource: auth.source };
6682
+ }
6683
+ } catch {
6684
+ return { state: "unavailable", tokenSource: auth.source };
6685
+ }
6686
+ return { state: "unavailable", tokenSource: auth.source };
6687
+ }
6688
+
6689
+ // src/delegate-runtime.ts
6690
+ import {
6691
+ SESSION_NAMESPACE_BINDING_MAX_ENTRIES,
6692
+ SESSION_NAMESPACE_BINDING_MAX_NAMESPACES as SESSION_NAMESPACE_BINDING_MAX_NAMESPACES2,
6693
+ createFileSessionNamespaceBindingStore
6694
+ } from "@remnic/core/session-namespace-bindings";
6695
+
6696
+ // src/delegate-namespaces.ts
6697
+ import {
6698
+ SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH
6699
+ } from "@remnic/core/session-namespace-bindings";
6700
+ import { log as log3 } from "@remnic/core/logger";
6701
+ async function withNamespace(namespace, body, resolveScopedNamespace) {
6702
+ const scoped = await resolveScopedNamespace(namespace || void 0);
6703
+ return scoped === void 0 ? body : { ...body, namespace: scoped };
6704
+ }
6705
+ function explicitSessionNamespaceFrom(sessionKey, event, ctx) {
6706
+ const eventSessionKey = typeof event.sessionKey === "string" ? event.sessionKey : void 0;
6707
+ const ctxSessionKey = typeof ctx.sessionKey === "string" ? ctx.sessionKey : void 0;
6708
+ const sources = eventSessionKey === sessionKey ? [event, ctx] : ctxSessionKey === sessionKey ? [ctx, event] : [ctx, event];
6709
+ for (const source of sources) {
6710
+ const sourceSessionKey = typeof source.sessionKey === "string" ? source.sessionKey : void 0;
6711
+ if (sourceSessionKey !== sessionKey) continue;
6712
+ const runtime = source.runtime;
6713
+ if (typeof runtime !== "object" || runtime === null) continue;
6714
+ const agent = runtime.agent;
6715
+ if (typeof agent !== "object" || agent === null) continue;
6716
+ const session = agent.session;
6717
+ if (typeof session !== "object" || session === null) continue;
6718
+ const namespace = session.namespace;
6719
+ if (namespace !== void 0 && typeof namespace !== "string") {
6720
+ throw new Error("delegate session namespace metadata must be a string");
6721
+ }
6722
+ return { namespace: typeof namespace === "string" ? namespace.trim() || void 0 : void 0 };
6723
+ }
6724
+ return void 0;
6725
+ }
6726
+ async function rememberedNamespacesFor(sessionKey, namespaceBindings) {
6727
+ return namespaceBindings.namespacesFor(sessionKey);
6728
+ }
6729
+ async function rememberNamespace(sessionKey, namespace, namespaceBindings) {
6730
+ if (namespace.length > SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH) {
6731
+ throw new Error(
6732
+ `delegate session namespace exceeds the daemon limit of ${SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH} characters`
6733
+ );
6734
+ }
6735
+ try {
6736
+ await namespaceBindings.remember(sessionKey, namespace);
6737
+ } catch (err) {
6738
+ log3.warn(`delegate namespace binding persistence failed: ${String(err)}`);
6739
+ throw err;
6740
+ }
6741
+ }
6742
+ async function sessionNamespaceFrom(sessionKey, event, ctx, fallback, namespaceBindings) {
6743
+ const explicit = explicitSessionNamespaceFrom(sessionKey, event, ctx);
6744
+ if (explicit !== void 0) {
6745
+ await rememberNamespace(sessionKey, explicit.namespace ?? "", namespaceBindings);
6746
+ return explicit.namespace;
6747
+ }
6748
+ const remembered = await rememberedNamespacesFor(sessionKey, namespaceBindings);
6749
+ return remembered.length > 0 ? remembered.at(-1) || void 0 : fallback.trim() || void 0;
6750
+ }
6751
+ async function lifecycleSessionNamespacesFrom(sessionKey, event, ctx, fallback, namespaceBindings) {
6752
+ const explicit = explicitSessionNamespaceFrom(sessionKey, event, ctx);
6753
+ if (explicit !== void 0) {
6754
+ await rememberNamespace(sessionKey, explicit.namespace ?? "", namespaceBindings);
6755
+ }
6756
+ const remembered = await rememberedNamespacesFor(sessionKey, namespaceBindings);
6757
+ if (explicit !== void 0) {
6758
+ const explicitNamespace = explicit.namespace ?? "";
6759
+ const namespaces = remembered.includes(explicitNamespace) ? remembered : [...remembered, explicitNamespace];
6760
+ return namespaces.map((namespace) => namespace || void 0);
6761
+ }
6762
+ if (remembered.length > 0) return remembered.map((namespace) => namespace || void 0);
6763
+ return [fallback.trim() || void 0];
6764
+ }
6765
+
6766
+ // src/delegate-daemon-target.ts
6767
+ function daemonTargetFor(bridge) {
6768
+ return {
6769
+ host: bridge.daemonHost,
6770
+ port: bridge.daemonPort,
6771
+ resolveAuthToken: () => {
6772
+ if (bridge.daemonAuthTokenOverride !== void 0) {
6773
+ const unit = bridge.daemonAuthUnit === void 0 ? void 0 : readUnitAuthToken(bridge.daemonAuthUnit);
6774
+ if (unit?.readable === true && unit.token !== void 0) {
6775
+ return { token: unit.token, source: "daemon configuration" };
6776
+ }
6777
+ if (unit === void 0 || unit.readable === false) {
6778
+ return { token: bridge.daemonAuthTokenOverride, source: "daemon configuration" };
6779
+ }
6780
+ if (bridge.daemonConfigPath !== void 0) {
6781
+ const configToken = readDaemonConfigAuthToken(bridge.daemonConfigPath);
6782
+ if (configToken !== void 0) {
6783
+ return { token: configToken, source: "daemon configuration" };
6784
+ }
6785
+ }
6786
+ }
6787
+ if (bridge.daemonAuthPrefersConfig && bridge.daemonConfigPath !== void 0) {
6788
+ const configToken = readDaemonConfigAuthToken(bridge.daemonConfigPath);
6789
+ if (configToken !== void 0) {
6790
+ return { token: configToken, source: "daemon configuration" };
6791
+ }
6792
+ }
6793
+ return loadDaemonAuth(bridge.daemonConfigPath);
6794
+ }
6795
+ };
6796
+ }
6797
+
6798
+ // src/delegate-flush-plan-ingest.ts
6799
+ import { lstat as lstat3, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
6800
+ import path7 from "path";
6801
+ import { log as log4 } from "@remnic/core/logger";
6802
+
6803
+ // src/memory-flush-plan.ts
6804
+ var DEFAULT_MAX_TURN_CHARS = 8e3;
6805
+ var MIN_MAX_TURN_CHARS = 1e3;
6806
+ var FLUSH_PROMPT = "Flush the recent OpenClaw transcript into Remnic memory by appending to the allowed flush-plan file only. Preserve durable user preferences, project facts, decisions, corrections, and commitments. Ignore runtime metadata, credentials, and transient command noise.";
6807
+ var FLUSH_SYSTEM_PROMPT = "You are Remnic's memory flush planner. Read the transcript and append concise durable memory notes to the file the write tool allows. Do not create files, directories, or dated paths; use only the allowed flush-plan file. Ignore runtime metadata, credentials, transient command noise, and content that is not worth remembering.";
6808
+ function buildMemoryFlushPlan(options) {
6809
+ const maxTurnChars = typeof options.extractionMaxTurnChars === "number" && Number.isFinite(options.extractionMaxTurnChars) ? Math.max(MIN_MAX_TURN_CHARS, Math.floor(options.extractionMaxTurnChars)) : DEFAULT_MAX_TURN_CHARS;
6810
+ const flushModel = typeof options.flushModel === "string" && options.flushModel.length > 0 ? options.flushModel : void 0;
6811
+ return {
6812
+ softThresholdTokens: 24e3,
6813
+ forceFlushTranscriptBytes: Math.max(16384, maxTurnChars * 4),
6814
+ reserveTokensFloor: 2e3,
6815
+ ...flushModel ? { model: flushModel } : {},
6816
+ prompt: FLUSH_PROMPT,
6817
+ systemPrompt: FLUSH_SYSTEM_PROMPT,
6818
+ relativePath: ["state", "plugins", options.serviceId, "flush-plan.md"].join("/")
6819
+ };
6820
+ }
6821
+
6822
+ // src/delegate-flush-plan-ingest.ts
6823
+ var MAX_OBSERVE_CHUNK_BYTES = 96 * 1024;
6824
+ var MIN_OBSERVE_CHUNK_BYTES = 4 * 1024;
6825
+ async function ingestFlushPlanNotes(options) {
6826
+ if (options.workspaceDir === void 0) return;
6827
+ const planPath = path7.join(
6828
+ options.workspaceDir,
6829
+ ...buildMemoryFlushPlan({ serviceId: options.serviceId }).relativePath.split("/")
6830
+ );
6831
+ if (!await isLinkFreeUnder(options.workspaceDir, planPath)) {
6832
+ log4.warn(
6833
+ `[${options.serviceId}] flush-plan ingestion skipped: ${planPath}, a parent, or the workspace root is a symlink`
6834
+ );
6835
+ return;
6836
+ }
6837
+ let pending = await readPlan(planPath);
6838
+ if (pending === void 0 || pending.trim().length === 0) return;
6839
+ let chunkBytes = MAX_OBSERVE_CHUNK_BYTES;
6840
+ while (pending.trim().length > 0) {
6841
+ const timeoutMs = options.remainingTimeoutMs();
6842
+ if (timeoutMs <= 0) {
6843
+ log4.warn(
6844
+ `[${options.serviceId}] flush-plan ingestion stopped: the caller's deadline is spent; the remainder drains on the next flush`
6845
+ );
6846
+ return;
6847
+ }
6848
+ const [chunk] = chunkOnLineBoundaries(pending, chunkBytes);
6849
+ if (chunk === void 0) return;
6850
+ const accepted = await postJson(
6851
+ options.target,
6852
+ options.serviceId,
6853
+ "/engram/v1/observe",
6854
+ {
6855
+ sessionKey: options.sessionKey,
6856
+ messages: [{ role: "user", content: chunk }],
6857
+ ...options.namespace === void 0 ? {} : { namespace: options.namespace }
6858
+ },
6859
+ timeoutMs
6860
+ );
6861
+ if (accepted === null) {
6862
+ if (chunkBytes > MIN_OBSERVE_CHUNK_BYTES && chunkOnLineBoundaries(pending, chunkBytes).length > 1) {
6863
+ chunkBytes = Math.max(MIN_OBSERVE_CHUNK_BYTES, Math.floor(chunkBytes / 2));
6864
+ continue;
6865
+ }
6866
+ log4.warn(
6867
+ `[${options.serviceId}] flush-plan notes were rejected by the daemon; keeping the remainder for the next flush`
6868
+ );
6869
+ return;
6870
+ }
6871
+ pending = await commitAcceptedPrefix(planPath, chunk) ?? "";
6872
+ }
6873
+ }
6874
+ async function readPlan(planPath) {
6875
+ try {
6876
+ return await readFile2(planPath, "utf8");
6877
+ } catch {
6878
+ return void 0;
6879
+ }
6880
+ }
6881
+ async function commitAcceptedPrefix(planPath, accepted) {
6882
+ const current = await readPlan(planPath);
6883
+ if (current === void 0) return void 0;
6884
+ const remainder = current.startsWith(accepted) ? current.slice(accepted.length) : current;
6885
+ await writeFile2(planPath, remainder, "utf8");
6886
+ return current.startsWith(accepted) ? remainder : void 0;
6887
+ }
6888
+ function chunkOnLineBoundaries(text, limit) {
6889
+ if (Buffer.byteLength(text, "utf8") <= limit) return [text];
6890
+ const chunks = [];
6891
+ let current = "";
6892
+ for (const line of text.split(/(?<=\n)/)) {
6893
+ if (current !== "" && Buffer.byteLength(current + line, "utf8") > limit) {
6894
+ chunks.push(current);
6895
+ current = "";
6896
+ }
6897
+ current += line;
6898
+ }
6899
+ if (current !== "") chunks.push(current);
6900
+ return chunks;
6901
+ }
6902
+ async function isLinkFreeUnder(root, target) {
6903
+ const relative = path7.relative(root, target);
6904
+ if (relative.startsWith("..") || path7.isAbsolute(relative)) return false;
6905
+ let current = root;
6906
+ try {
6907
+ if ((await lstat3(current)).isSymbolicLink()) return false;
6908
+ } catch {
6909
+ return true;
6910
+ }
6911
+ for (const segment of relative.split(path7.sep)) {
6912
+ current = path7.join(current, segment);
6913
+ try {
6914
+ if ((await lstat3(current)).isSymbolicLink()) return false;
6915
+ } catch {
6916
+ return true;
6917
+ }
6918
+ }
6919
+ return true;
6920
+ }
6921
+
6922
+ // src/delegate-runtime.ts
6923
+ import { createFileToggleStore } from "@remnic/core/session-toggles";
6924
+
5709
6925
  // src/transcript-turns.ts
5710
6926
  function extractLastTurn(messages) {
5711
6927
  let lastUserIdx = -1;
@@ -5714,62 +6930,499 @@ function extractLastTurn(messages) {
5714
6930
  lastUserIdx = i;
5715
6931
  break;
5716
6932
  }
5717
- }
5718
- return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages.slice(-2);
6933
+ }
6934
+ return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages.slice(-2);
6935
+ }
6936
+ function extractTextContent(msg) {
6937
+ if (typeof msg.content === "string") return msg.content;
6938
+ if (Array.isArray(msg.content)) {
6939
+ return msg.content.filter(
6940
+ (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string"
6941
+ ).map((block) => block.text).join("\n");
6942
+ }
6943
+ return "";
6944
+ }
6945
+
6946
+ // src/delegate-capability.ts
6947
+ import { readFile as readFile3 } from "fs/promises";
6948
+ import { log as log5 } from "@remnic/core/logger";
6949
+ var HEALTH_CACHE_TTL_MS = 3e4;
6950
+ var SEARCH_CANDIDATE_CAP = 25e3;
6951
+ function searchCandidateCeiling(budget) {
6952
+ return Math.max(SEARCH_CANDIDATE_CAP, budget * 2);
6953
+ }
6954
+ var HEALTH_FAILURE_BACKOFF_MS = 5e3;
6955
+ var LOCAL_READ_VERDICT_TTL_MS = 3e4;
6956
+ function asRecord(value) {
6957
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6958
+ }
6959
+ function readHealth(payload) {
6960
+ const qmd = asRecord(payload.qmd) ?? {};
6961
+ const searchBackend = payload.searchBackend === "qmd" ? "qmd" : "builtin";
6962
+ const qmdEnabled = searchBackend === "qmd" && payload.qmdEnabled !== false;
6963
+ return {
6964
+ memoryDir: typeof payload.memoryDir === "string" ? payload.memoryDir : void 0,
6965
+ defaultNamespace: typeof payload.defaultNamespace === "string" ? payload.defaultNamespace : void 0,
6966
+ namespacesEnabled: typeof payload.namespacesEnabled === "boolean" ? payload.namespacesEnabled : void 0,
6967
+ searchBackend,
6968
+ qmdEnabled,
6969
+ // `active && !degraded` is the daemon's own "search will actually answer"
6970
+ // signal. A missing `active` means an older daemon, so fall back to
6971
+ // `enabled` rather than reporting a false outage.
6972
+ qmdAvailable: qmdEnabled && (typeof qmd.active === "boolean" ? qmd.active : qmd.enabled !== false) && qmd.degraded !== true,
6973
+ qmdDebug: typeof qmd.debugStatus === "string" ? qmd.debugStatus : void 0
6974
+ };
6975
+ }
6976
+ function createDelegateMemoryCapability(options) {
6977
+ const { target, serviceId } = options;
6978
+ const now = options.now ?? Date.now;
6979
+ let daemonScope;
6980
+ const sharedScope = () => {
6981
+ const dir = health.memoryDir ?? options.memoryDir;
6982
+ if (daemonScope?.dir !== dir) {
6983
+ daemonScope = {
6984
+ dir,
6985
+ scope: createMemoryReadScope({
6986
+ // The daemon's own directory comes first, so a relative hit resolves
6987
+ // in the frame the daemon returned it in...
6988
+ memoryDir: dir,
6989
+ workspaceDir: options.workspaceDir,
6990
+ // The gateway's own workspace is NOT a readable root here: health
6991
+ // validates `memoryDir` only, so a relative path absent from the
6992
+ // daemon corpus must not be served from a workspace the daemon
6993
+ // never searched or authorized.
6994
+ includeWorkspaceRoot: false,
6995
+ // ...and the corpus ROOT stays readable, so a session bound to a
6996
+ // NON-default namespace can still open the absolute hits its own
6997
+ // search returns from `<root>/namespaces/<other>`.
6998
+ additionalRoots: [options.memoryDir]
6999
+ })
7000
+ };
7001
+ }
7002
+ return daemonScope.scope;
7003
+ };
7004
+ let health = {
7005
+ memoryDir: options.memoryDir,
7006
+ searchBackend: options.configuredSearchBackend,
7007
+ qmdEnabled: options.configuredSearchBackend === "qmd",
7008
+ qmdAvailable: options.configuredSearchBackend === "qmd"
7009
+ // Deliberately NOT seeded from the plugin's config: that describes this
7010
+ // plugin's own deployment, never the daemon's partitioning. Only a
7011
+ // successful probe can prove a flat corpus, and until one does, an absent
7012
+ // namespace fails closed.
7013
+ };
7014
+ let healthExpiresAt = 0;
7015
+ let healthCacheIsFailure = false;
7016
+ let healthEverResolved = false;
7017
+ const localReadVerdictAt = /* @__PURE__ */ new Map();
7018
+ let healthInFlight;
7019
+ let lastHealthFailure;
7020
+ const daemonIsLocal = isLoopbackDaemonHost(target.host);
7021
+ let corpusShared = daemonIsLocal ? void 0 : false;
7022
+ let reportedCorpusMismatch = false;
7023
+ const requireSharedCorpus = (surface) => {
7024
+ if (corpusShared === true) return;
7025
+ const detail = corpusShared !== false ? "the daemon's corpus has not been confirmed yet" : daemonIsLocal ? `daemon serves ${health.memoryDir ?? "an unknown memoryDir"}, plugin is configured for ${options.memoryDir}` : `daemon at ${target.host} is not local, so its corpus is not this host's files`;
7026
+ throw new Error(`delegate ${surface} unavailable: ${detail}`);
7027
+ };
7028
+ const requireLocalReadAuthorized = async (surface, operations) => {
7029
+ if (options.verifyNamespaceAuthorization === void 0) return;
7030
+ const namespace = health.defaultNamespace ?? "";
7031
+ const verdictKey = `${target.resolveAuthToken().token}\0${operations.join(",")}\0${namespace}`;
7032
+ const cachedAt = localReadVerdictAt.get(verdictKey);
7033
+ if (cachedAt !== void 0 && now() - cachedAt >= LOCAL_READ_VERDICT_TTL_MS) {
7034
+ substitutedNamespaceVerdicts.delete(verdictKey);
7035
+ localReadVerdictAt.delete(verdictKey);
7036
+ }
7037
+ if (!substitutedNamespaceVerdicts.has(verdictKey)) {
7038
+ substitutedNamespaceVerdicts.set(
7039
+ verdictKey,
7040
+ await options.verifyNamespaceAuthorization(namespace, void 0, operations)
7041
+ );
7042
+ localReadVerdictAt.set(verdictKey, now());
7043
+ }
7044
+ if (substitutedNamespaceVerdicts.get(verdictKey) !== true) {
7045
+ throw new Error(
7046
+ `delegate ${surface} unavailable: the delegate token's authorization for ${operations.join(", ")} on the daemon's corpus ${substitutedNamespaceVerdicts.get(verdictKey) === false ? "was refused" : "could not be confirmed"}`
7047
+ );
7048
+ }
7049
+ };
7050
+ const requireSingleCorpusNamespacing = (surface) => {
7051
+ if (health.namespacesEnabled === false) return;
7052
+ throw new Error(
7053
+ `delegate ${surface} unavailable: the daemon partitions namespaces (${health.namespacesEnabled === true ? `default: ${health.defaultNamespace ?? "unreported"}` : "namespacing unreported"}) and this surface carries no session, so a local read cannot be authorized for the caller's namespace`
7054
+ );
7055
+ };
7056
+ const requireScopedNamespace = (explicit) => {
7057
+ const namespace = explicit ?? health.defaultNamespace;
7058
+ if (namespace === void 0 && health.namespacesEnabled !== false) {
7059
+ throw new Error(
7060
+ "delegate request unavailable: the daemon's default namespace is unknown, so the session scope cannot be resolved"
7061
+ );
7062
+ }
7063
+ return namespace;
7064
+ };
7065
+ const substitutedNamespaceVerdicts = /* @__PURE__ */ new Map();
7066
+ const resolveScopedNamespaceChecked = async (explicit, timeoutMs, healthIsFresh = true, operations) => {
7067
+ if (!healthIsFresh && explicit === void 0) {
7068
+ throw new Error(
7069
+ "delegate request unavailable: the daemon's namespace posture could not be confirmed within the caller's deadline, so an unscoped request is not safe"
7070
+ );
7071
+ }
7072
+ const namespace = requireScopedNamespace(explicit);
7073
+ if (explicit !== void 0 || namespace === void 0) return namespace;
7074
+ if (options.verifyNamespaceAuthorization === void 0) return namespace;
7075
+ const verdictKey = `${target.resolveAuthToken().token}\0${(operations ?? []).join(",")}\0${namespace}`;
7076
+ if (!substitutedNamespaceVerdicts.has(verdictKey)) {
7077
+ if (timeoutMs !== void 0 && timeoutMs <= 0) {
7078
+ throw new Error(
7079
+ `delegate request unavailable: the delegate token's authorization for the daemon's default namespace (${namespace}) could not be verified within the caller's deadline, so an unscoped request is not safe`
7080
+ );
7081
+ }
7082
+ substitutedNamespaceVerdicts.set(
7083
+ verdictKey,
7084
+ await options.verifyNamespaceAuthorization(namespace, timeoutMs, operations)
7085
+ );
7086
+ }
7087
+ if (substitutedNamespaceVerdicts.get(verdictKey) === false) {
7088
+ throw new Error(
7089
+ `delegate request unavailable: this session has no namespace binding and the daemon's default (${namespace}) is not authorized for the delegate token \u2014 bind the session explicitly or configure the namespace this deployment should use`
7090
+ );
7091
+ }
7092
+ return namespace;
7093
+ };
7094
+ const refreshHealth = async (timeoutMs, revalidate = false) => {
7095
+ if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || !Number.isFinite(timeoutMs))) {
7096
+ throw new Error(
7097
+ `delegate request rejected (timeoutMs must be a finite integer): ${String(timeoutMs)}`
7098
+ );
7099
+ }
7100
+ if (now() < healthExpiresAt) {
7101
+ if (healthCacheIsFailure) return false;
7102
+ if (!revalidate) return true;
7103
+ }
7104
+ if (timeoutMs !== void 0 && timeoutMs <= 0) return false;
7105
+ if (healthInFlight !== void 0) {
7106
+ if (timeoutMs === void 0) {
7107
+ await healthInFlight;
7108
+ return true;
7109
+ }
7110
+ const TIMED_OUT = /* @__PURE__ */ Symbol("timed-out");
7111
+ const outcome = await Promise.race([
7112
+ healthInFlight.then(() => void 0),
7113
+ new Promise((resolve) => {
7114
+ const timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
7115
+ timer.unref?.();
7116
+ })
7117
+ ]);
7118
+ return outcome !== TIMED_OUT;
7119
+ }
7120
+ healthInFlight = (async () => {
7121
+ try {
7122
+ const response = await fetch(daemonUrl(target, "/engram/v1/health"), {
7123
+ headers: daemonAuthHeaders(target),
7124
+ signal: AbortSignal.timeout(
7125
+ timeoutMs === void 0 ? options.healthTimeoutMs : Math.min(options.healthTimeoutMs, timeoutMs)
7126
+ )
7127
+ });
7128
+ if (!response.ok) {
7129
+ await response.body?.cancel();
7130
+ throw new Error(`daemon /engram/v1/health responded ${response.status}`);
7131
+ }
7132
+ const payload = await response.json();
7133
+ const healthPayload = asRecord(payload);
7134
+ if (!healthPayload) {
7135
+ throw new Error("daemon /engram/v1/health returned a malformed envelope");
7136
+ }
7137
+ health = readHealth(healthPayload);
7138
+ healthExpiresAt = now() + HEALTH_CACHE_TTL_MS;
7139
+ healthCacheIsFailure = false;
7140
+ healthEverResolved = true;
7141
+ lastHealthFailure = void 0;
7142
+ corpusShared = daemonIsLocal && health.memoryDir !== void 0 && daemonServesCorpus(options.memoryDir, health.memoryDir);
7143
+ if (!corpusShared && daemonIsLocal && !reportedCorpusMismatch) {
7144
+ reportedCorpusMismatch = true;
7145
+ log5.error(
7146
+ `[${serviceId}] delegate capability: the daemon does not serve this plugin's memoryDir (daemon: ${health.memoryDir ?? "unreported"}, plugin: ${options.memoryDir}) \u2014 file-backed reads and public artifacts are disabled; search still runs through the daemon`
7147
+ );
7148
+ }
7149
+ } catch (err) {
7150
+ health = {
7151
+ ...health,
7152
+ namespacesEnabled: void 0,
7153
+ defaultNamespace: void 0,
7154
+ memoryDir: void 0
7155
+ };
7156
+ corpusShared = daemonIsLocal ? void 0 : false;
7157
+ healthExpiresAt = now() + HEALTH_FAILURE_BACKOFF_MS;
7158
+ healthCacheIsFailure = true;
7159
+ healthEverResolved = true;
7160
+ const message = `[${serviceId}] delegate capability health probe failed: ${String(err)}`;
7161
+ if (message !== lastHealthFailure) {
7162
+ lastHealthFailure = message;
7163
+ log5.warn(message);
7164
+ }
7165
+ } finally {
7166
+ healthInFlight = void 0;
7167
+ }
7168
+ })();
7169
+ await healthInFlight;
7170
+ return true;
7171
+ };
7172
+ const search = async (query, opts) => {
7173
+ if (opts?.minScore !== void 0 && !Number.isFinite(opts.minScore)) {
7174
+ throw new Error(
7175
+ `delegate search rejected (minScore must be a finite number): ${String(opts.minScore)}`
7176
+ );
7177
+ }
7178
+ if (opts?.maxResults !== void 0 && (!Number.isInteger(opts.maxResults) || opts.maxResults < 0)) {
7179
+ throw new Error(
7180
+ `delegate search rejected (maxResults must be a non-negative integer): ${String(opts.maxResults)}`
7181
+ );
7182
+ }
7183
+ const searchDeadline = now() + options.searchTimeoutMs;
7184
+ const searchRemaining = () => searchDeadline - now();
7185
+ const requestedResults = typeof opts?.maxResults === "number" ? opts.maxResults : void 0;
7186
+ const searchScope = await options.resolveSearchNamespace(opts?.sessionKey);
7187
+ const scopeIsFresh = searchScope === void 0 ? await refreshHealth(searchRemaining(), true) : true;
7188
+ const namespace = await resolveScopedNamespaceChecked(
7189
+ searchScope,
7190
+ searchRemaining(),
7191
+ scopeIsFresh,
7192
+ ["memory_search"]
7193
+ );
7194
+ if (opts?.maxResults === 0) return [];
7195
+ const searchMode = opts?.qmdSearchModeOverride === "vsearch" ? "vector" : "search";
7196
+ const fetchPage = async (limit2) => {
7197
+ const remaining = searchRemaining();
7198
+ if (remaining <= 0) {
7199
+ throw new Error(
7200
+ `delegate search unavailable: the search budget of ${options.searchTimeoutMs}ms is spent`
7201
+ );
7202
+ }
7203
+ const response = await fetch(daemonUrl(target, "/engram/v1/memories/search"), {
7204
+ method: "POST",
7205
+ headers: { ...daemonAuthHeaders(target), "Content-Type": "application/json" },
7206
+ body: JSON.stringify({
7207
+ query,
7208
+ ...limit2 === void 0 ? {} : { maxResults: limit2 },
7209
+ // Same override mapping the embedded manager applies, so a host asking
7210
+ // for vector or lexical ranking gets the same semantics in either mode.
7211
+ mode: searchMode,
7212
+ ...namespace === void 0 ? {} : { namespace }
7213
+ }),
7214
+ // What is LEFT of the shared budget, not a fresh one.
7215
+ signal: AbortSignal.timeout(remaining)
7216
+ });
7217
+ if (!response.ok) {
7218
+ await response.body?.cancel();
7219
+ throw new Error(`daemon /engram/v1/memories/search responded ${response.status}`);
7220
+ }
7221
+ const payload = await response.json();
7222
+ const body = asRecord(payload);
7223
+ if (!Array.isArray(body?.results)) {
7224
+ throw new Error("daemon /engram/v1/memories/search returned a malformed envelope");
7225
+ }
7226
+ return body.results;
7227
+ };
7228
+ const keep = (rawResults) => {
7229
+ const kept2 = [];
7230
+ for (const raw of rawResults) {
7231
+ const hit = asRecord(raw);
7232
+ if (!hit || typeof hit.path !== "string" || hit.path.trim() === "") {
7233
+ throw new Error("daemon /engram/v1/memories/search returned a malformed result entry");
7234
+ }
7235
+ const rawPath = hit.path;
7236
+ const citation = sharedScope().relativizeToMemoryRoot(rawPath);
7237
+ if (isMemoryArtifactPath(citation)) continue;
7238
+ if (typeof hit.score !== "number" || !Number.isFinite(hit.score)) {
7239
+ throw new Error("daemon /engram/v1/memories/search returned a malformed result entry");
7240
+ }
7241
+ const score = hit.score;
7242
+ if (typeof opts?.minScore === "number" && score < opts.minScore) continue;
7243
+ kept2.push({
7244
+ // Absolute, so a follow-up readFile is unambiguous when the same
7245
+ // relative path exists under more than one allowed root.
7246
+ path: sharedScope().absolutize(rawPath),
7247
+ // The daemon's ranked search returns whole-memory hits with no line
7248
+ // span; the embedded runtime reports the same 1..1 default.
7249
+ startLine: 1,
7250
+ endLine: 1,
7251
+ score,
7252
+ snippet: typeof hit.snippet === "string" ? hit.snippet : "",
7253
+ source: isSessionsMemoryPath(citation) ? "sessions" : "memory",
7254
+ citation
7255
+ });
7256
+ }
7257
+ return kept2;
7258
+ };
7259
+ let kept = [];
7260
+ let budget = requestedResults;
7261
+ let limit = requestedResults;
7262
+ for (; ; ) {
7263
+ const rawResults = await fetchPage(limit);
7264
+ kept = keep(rawResults);
7265
+ budget ??= rawResults.length;
7266
+ if (kept.length >= budget) break;
7267
+ const served = limit ?? rawResults.length;
7268
+ if (rawResults.length === 0 || rawResults.length < served) break;
7269
+ const ceiling = searchCandidateCeiling(budget);
7270
+ if (served >= ceiling) break;
7271
+ limit = Math.min(served * 2, ceiling);
7272
+ }
7273
+ return kept.slice(0, budget);
7274
+ };
7275
+ const readMemoryFile = async (params) => {
7276
+ await refreshHealth(void 0, true);
7277
+ requireSharedCorpus("readFile");
7278
+ requireSingleCorpusNamespacing("readFile");
7279
+ await requireLocalReadAuthorized("readFile", ["memory_get"]);
7280
+ const requestedPath = sharedScope().normalizeWorkspacePath(params.relPath);
7281
+ const absolutePath = await sharedScope().resolveReadablePath(params.relPath);
7282
+ const allLines = (await readFile3(absolutePath, "utf8")).split(/\r?\n/);
7283
+ if (params.from !== void 0 && (!Number.isInteger(params.from) || params.from < 1)) {
7284
+ throw new Error(
7285
+ `memory read rejected (from must be a positive integer): ${String(params.from)}`
7286
+ );
7287
+ }
7288
+ if (params.lines !== void 0 && (!Number.isInteger(params.lines) || params.lines < 1)) {
7289
+ throw new Error(
7290
+ `memory read rejected (lines must be a positive integer): ${String(params.lines)}`
7291
+ );
7292
+ }
7293
+ const from = typeof params.from === "number" ? params.from : 1;
7294
+ const lines = typeof params.lines === "number" ? params.lines : void 0;
7295
+ const startIndex = from - 1;
7296
+ const endIndex = typeof lines === "number" ? startIndex + lines : allLines.length;
7297
+ const truncated = endIndex < allLines.length;
7298
+ return {
7299
+ text: allLines.slice(startIndex, endIndex).join("\n"),
7300
+ path: requestedPath,
7301
+ truncated: truncated || void 0,
7302
+ from,
7303
+ lines,
7304
+ nextFrom: truncated ? endIndex + 1 : void 0
7305
+ };
7306
+ };
7307
+ const status = () => {
7308
+ const usesQmd = health.searchBackend === "qmd";
7309
+ return {
7310
+ backend: usesQmd ? "qmd" : "builtin",
7311
+ provider: usesQmd ? "qmd" : "builtin",
7312
+ requestedProvider: usesQmd ? "qmd" : "builtin",
7313
+ model: usesQmd ? options.configuredQmdCommand : "builtin",
7314
+ dirty: false,
7315
+ workspaceDir: options.workspaceDir,
7316
+ dbPath: health.memoryDir ?? options.memoryDir,
7317
+ sources: ["memory"],
7318
+ sourceCounts: [],
7319
+ vector: usesQmd ? { enabled: true, available: health.qmdAvailable } : { enabled: false },
7320
+ fts: { enabled: true, available: usesQmd ? health.qmdAvailable : true },
7321
+ custom: {
7322
+ remnic: {
7323
+ bridgeMode: "delegate",
7324
+ daemon: `${target.host}:${target.port}`,
7325
+ qmdAvailable: health.qmdAvailable,
7326
+ qmdDebug: health.qmdDebug,
7327
+ memoryDir: health.memoryDir ?? options.memoryDir
7328
+ }
7329
+ }
7330
+ };
7331
+ };
7332
+ const manager = {
7333
+ search,
7334
+ readFile: readMemoryFile,
7335
+ status,
7336
+ // No `sync`: indexing belongs to the daemon in delegate mode. Omitting the
7337
+ // optional member is honest — the host will not call what is not offered.
7338
+ async probeEmbeddingAvailability() {
7339
+ await refreshHealth();
7340
+ if (health.searchBackend !== "qmd") return { ok: true };
7341
+ if (health.qmdAvailable) return { ok: true };
7342
+ return { ok: false, error: health.qmdDebug ?? "Remnic daemon QMD backend unavailable" };
7343
+ },
7344
+ async probeVectorAvailability() {
7345
+ await refreshHealth();
7346
+ return health.searchBackend === "qmd" && health.qmdAvailable;
7347
+ },
7348
+ async close() {
7349
+ }
7350
+ };
7351
+ return {
7352
+ resolveScopedNamespace: async (explicit, timeoutMs, operations) => {
7353
+ if (explicit !== void 0) {
7354
+ return await resolveScopedNamespaceChecked(explicit, timeoutMs, true, operations);
7355
+ }
7356
+ const deadline = timeoutMs === void 0 ? void 0 : now() + timeoutMs;
7357
+ const fresh = await refreshHealth(timeoutMs, true);
7358
+ const remaining = deadline === void 0 ? void 0 : Math.floor(deadline - now());
7359
+ return await resolveScopedNamespaceChecked(void 0, remaining, fresh, operations);
7360
+ },
7361
+ runtime: {
7362
+ async getMemorySearchManager() {
7363
+ if (!healthEverResolved) await refreshHealth();
7364
+ else void refreshHealth().catch(() => void 0);
7365
+ return { manager };
7366
+ },
7367
+ resolveMemoryBackendConfig() {
7368
+ return health.searchBackend === "qmd" ? { backend: "qmd", qmd: { command: options.configuredQmdCommand } } : { backend: "builtin" };
7369
+ },
7370
+ async closeAllMemorySearchManagers() {
7371
+ }
7372
+ },
7373
+ flushPlanResolver: () => buildMemoryFlushPlan({
7374
+ serviceId,
7375
+ extractionMaxTurnChars: options.extractionMaxTurnChars,
7376
+ flushModel: options.flushModel
7377
+ }),
7378
+ listArtifacts: async () => {
7379
+ try {
7380
+ await refreshHealth(void 0, true);
7381
+ requireSharedCorpus("publicArtifacts");
7382
+ requireSingleCorpusNamespacing("publicArtifacts");
7383
+ await requireLocalReadAuthorized("publicArtifacts", ["memory_get"]);
7384
+ return await listRemnicPublicArtifacts({
7385
+ memoryDir: health.memoryDir ?? options.memoryDir,
7386
+ workspaceDir: options.workspaceDir,
7387
+ agentIds: options.agentIds
7388
+ });
7389
+ } catch (err) {
7390
+ log5.error(`[${serviceId}] delegate publicArtifacts.listArtifacts failed`, err);
7391
+ return [];
7392
+ }
7393
+ },
7394
+ promptBuilder: (params) => options.readPromptLines(params?.sessionKey ?? "default")
7395
+ };
5719
7396
  }
5720
- function extractTextContent(msg) {
5721
- if (typeof msg.content === "string") return msg.content;
5722
- if (Array.isArray(msg.content)) {
5723
- return msg.content.filter(
5724
- (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string"
5725
- ).map((block) => block.text).join("\n");
7397
+ function registerDelegateMemoryCapability(api, options) {
7398
+ const built = createDelegateMemoryCapability(options);
7399
+ const hasUnified = typeof api.registerMemoryCapability === "function";
7400
+ const hasRuntime = typeof api.registerMemoryRuntime === "function";
7401
+ const hasFlushPlan = typeof api.registerMemoryFlushPlan === "function";
7402
+ if (!hasUnified && !hasRuntime && !hasFlushPlan) {
7403
+ log5.debug(
7404
+ `[${options.serviceId}] delegate: host exposes no memory capability surface \u2014 nothing to register`
7405
+ );
7406
+ return built;
7407
+ }
7408
+ if (hasUnified) {
7409
+ api.registerMemoryCapability?.({
7410
+ ...options.allowPromptInjection ? { promptBuilder: built.promptBuilder } : {},
7411
+ flushPlanResolver: built.flushPlanResolver,
7412
+ runtime: built.runtime,
7413
+ publicArtifacts: { listArtifacts: built.listArtifacts }
7414
+ });
5726
7415
  }
5727
- return "";
7416
+ if (hasRuntime) api.registerMemoryRuntime?.(built.runtime);
7417
+ if (hasFlushPlan) api.registerMemoryFlushPlan?.(built.flushPlanResolver);
7418
+ const surface = hasUnified ? "memory capability with publicArtifacts provider" : "split memory runtime/flush-plan surfaces";
7419
+ const builder = options.allowPromptInjection ? " and promptBuilder" : " (promptBuilder omitted \u2014 injection disabled by policy)";
7420
+ log5.info(`[${options.serviceId}] delegate: registered daemon-backed ${surface}${builder}`);
7421
+ return built;
5728
7422
  }
5729
7423
 
5730
7424
  // src/delegate-runtime.ts
5731
7425
  var DELEGATE_BATCH_FLUSH_CACHE_TTL_MS = 3e4;
5732
- var DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS = [
5733
- "recall",
5734
- "observe",
5735
- "lcm_compaction_flush"
5736
- ];
5737
- function daemonUrl(target, pathname) {
5738
- const host = target.host.includes(":") && !target.host.startsWith("[") ? `[${target.host}]` : target.host;
5739
- return `http://${host}:${target.port}${pathname}`;
5740
- }
5741
- var daemonAuthFailureLogKeys = /* @__PURE__ */ new Set();
5742
- function reportDaemonAuthorizationFailure(serviceId, pathname, status, tokenSource) {
5743
- const key = `${serviceId}:${pathname}:${status}:${tokenSource}`;
5744
- if (daemonAuthFailureLogKeys.has(key)) return;
5745
- daemonAuthFailureLogKeys.add(key);
5746
- log2.error(
5747
- `delegate ${pathname} authorization failed (${status}; token source: ${tokenSource})`
5748
- );
5749
- }
5750
- async function probeDelegateAuthorization(target, namespace = "", operations = DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS) {
5751
- const auth = target.resolveAuthToken();
5752
- const headers = auth.token ? { Authorization: `Bearer ${auth.token}` } : void 0;
5753
- const query = new URLSearchParams();
5754
- for (const operation of operations) query.append("op", operation);
5755
- query.set("namespace", namespace);
5756
- try {
5757
- const response = await fetch(daemonUrl(target, `/engram/v1/authorization?${query}`), {
5758
- headers,
5759
- signal: AbortSignal.timeout(2e3)
5760
- });
5761
- await response.body?.cancel();
5762
- if (response.status === 200) {
5763
- return { state: "authorized", tokenSource: auth.source };
5764
- }
5765
- if (response.status === 401 || response.status === 403) {
5766
- return { state: "unauthorized", status: response.status, tokenSource: auth.source };
5767
- }
5768
- } catch {
5769
- return { state: "unavailable", tokenSource: auth.source };
5770
- }
5771
- return { state: "unavailable", tokenSource: auth.source };
5772
- }
5773
7426
  async function postJson(target, serviceId, pathname, body, timeoutMs) {
5774
7427
  const headers = { "Content-Type": "application/json" };
5775
7428
  const auth = target.resolveAuthToken();
@@ -5853,69 +7506,6 @@ function cwdFrom(event, ctx, fallback) {
5853
7506
  }
5854
7507
  return fallback;
5855
7508
  }
5856
- function withNamespace(namespace, body) {
5857
- return namespace ? { ...body, namespace } : body;
5858
- }
5859
- function explicitSessionNamespaceFrom(sessionKey, event, ctx) {
5860
- const eventSessionKey = typeof event.sessionKey === "string" ? event.sessionKey : void 0;
5861
- const ctxSessionKey = typeof ctx.sessionKey === "string" ? ctx.sessionKey : void 0;
5862
- const sources = eventSessionKey === sessionKey ? [event, ctx] : ctxSessionKey === sessionKey ? [ctx, event] : [ctx, event];
5863
- for (const source of sources) {
5864
- const sourceSessionKey = typeof source.sessionKey === "string" ? source.sessionKey : void 0;
5865
- if (sourceSessionKey !== sessionKey) continue;
5866
- const runtime = source.runtime;
5867
- if (typeof runtime !== "object" || runtime === null) continue;
5868
- const agent = runtime.agent;
5869
- if (typeof agent !== "object" || agent === null) continue;
5870
- const session = agent.session;
5871
- if (typeof session !== "object" || session === null) continue;
5872
- const namespace = session.namespace;
5873
- if (namespace !== void 0 && typeof namespace !== "string") {
5874
- throw new Error("delegate session namespace metadata must be a string");
5875
- }
5876
- return { namespace: typeof namespace === "string" ? namespace.trim() || void 0 : void 0 };
5877
- }
5878
- return void 0;
5879
- }
5880
- async function rememberedNamespacesFor(sessionKey, namespaceBindings) {
5881
- return namespaceBindings.namespacesFor(sessionKey);
5882
- }
5883
- async function rememberNamespace(sessionKey, namespace, namespaceBindings) {
5884
- if (namespace.length > SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH) {
5885
- throw new Error(
5886
- `delegate session namespace exceeds the daemon limit of ${SESSION_NAMESPACE_BINDING_MAX_NAMESPACE_LENGTH} characters`
5887
- );
5888
- }
5889
- try {
5890
- await namespaceBindings.remember(sessionKey, namespace);
5891
- } catch (err) {
5892
- log2.warn(`delegate namespace binding persistence failed: ${String(err)}`);
5893
- throw err;
5894
- }
5895
- }
5896
- async function sessionNamespaceFrom(sessionKey, event, ctx, fallback, namespaceBindings) {
5897
- const explicit = explicitSessionNamespaceFrom(sessionKey, event, ctx);
5898
- if (explicit !== void 0) {
5899
- await rememberNamespace(sessionKey, explicit.namespace ?? "", namespaceBindings);
5900
- return explicit.namespace;
5901
- }
5902
- const remembered = await rememberedNamespacesFor(sessionKey, namespaceBindings);
5903
- return remembered.length > 0 ? remembered.at(-1) || void 0 : fallback.trim() || void 0;
5904
- }
5905
- async function lifecycleSessionNamespacesFrom(sessionKey, event, ctx, fallback, namespaceBindings) {
5906
- const explicit = explicitSessionNamespaceFrom(sessionKey, event, ctx);
5907
- if (explicit !== void 0) {
5908
- await rememberNamespace(sessionKey, explicit.namespace ?? "", namespaceBindings);
5909
- }
5910
- const remembered = await rememberedNamespacesFor(sessionKey, namespaceBindings);
5911
- if (explicit !== void 0) {
5912
- const explicitNamespace = explicit.namespace ?? "";
5913
- const namespaces = remembered.includes(explicitNamespace) ? remembered : [...remembered, explicitNamespace];
5914
- return namespaces.map((namespace) => namespace || void 0);
5915
- }
5916
- if (remembered.length > 0) return remembered.map((namespace) => namespace || void 0);
5917
- return [fallback.trim() || void 0];
5918
- }
5919
7509
  function readContextComposition(response, fallbackContext) {
5920
7510
  const candidate = response.contextComposition;
5921
7511
  if (typeof candidate !== "object" || candidate === null || !("context" in candidate) || typeof candidate.context !== "string") {
@@ -5930,28 +7520,82 @@ function registerDelegateRuntime(api, options) {
5930
7520
  const { target, namespace, namespaceBindings } = options;
5931
7521
  const now = options.now ?? Date.now;
5932
7522
  if (options.passive) {
5933
- log2.info(
7523
+ log6.info(
5934
7524
  `[${options.serviceId}] bridge mode delegate: memory slot not owned \u2014 passive, no hooks registered`
5935
7525
  );
5936
7526
  return;
5937
7527
  }
5938
- if (options.allowPromptInjection && options.recallBudgetChars !== 0) {
5939
- const promptLinesBySession = /* @__PURE__ */ new Map();
5940
- const useSectionBuilder = typeof api.registerMemoryPromptSection === "function";
7528
+ const promptLinesBySession = /* @__PURE__ */ new Map();
7529
+ const promptInjectionEnabled = options.allowPromptInjection && options.recallBudgetChars !== 0;
7530
+ const useSectionBuilder = typeof api.registerMemoryPromptSection === "function";
7531
+ const useCapabilityBuilder = !useSectionBuilder && typeof api.registerMemoryCapability === "function";
7532
+ const cachePromptLines = useSectionBuilder || useCapabilityBuilder;
7533
+ const capability = registerDelegateMemoryCapability(api, {
7534
+ serviceId: options.serviceId,
7535
+ target,
7536
+ namespace,
7537
+ // Capability searches scope through the SAME per-session binding history
7538
+ // the hooks use (the non-explicit branch of sessionNamespaceFrom): the
7539
+ // host hands the runtime a sessionKey but no event/ctx to read an explicit
7540
+ // namespace from, so the remembered binding — else the registration-wide
7541
+ // fallback — is the correct scope.
7542
+ resolveSearchNamespace: async (sessionKey) => {
7543
+ if (typeof sessionKey === "string" && sessionKey.trim().length > 0) {
7544
+ const remembered = await rememberedNamespacesFor(sessionKey, namespaceBindings);
7545
+ if (remembered.length > 0) return remembered.at(-1) || void 0;
7546
+ }
7547
+ return namespace.trim() || void 0;
7548
+ },
7549
+ // The daemon's own namespace-aware probe, so a substituted default is
7550
+ // proven usable before the first search rather than 403-ing on it.
7551
+ verifyNamespaceAuthorization: async (candidate, timeoutMs, operations) => {
7552
+ const probe = await probeDelegateAuthorization(
7553
+ target,
7554
+ candidate,
7555
+ // What the caller is about to do. A token that grants recall/observe/
7556
+ // flush but not memory_search must not have those rejected locally.
7557
+ operations ?? DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS,
7558
+ timeoutMs
7559
+ );
7560
+ return probe.state === "unavailable" ? void 0 : probe.state === "authorized";
7561
+ },
7562
+ memoryDir: options.capability.memoryDir,
7563
+ workspaceDir: options.capability.workspaceDir,
7564
+ agentIds: options.capability.agentIds,
7565
+ allowPromptInjection: promptInjectionEnabled,
7566
+ // The section builder owns the destructive read when it exists; otherwise
7567
+ // the capability builder IS the sole consumer and must evict, or a stale
7568
+ // section would be re-injected on the next turn.
7569
+ readPromptLines: (sessionKey) => {
7570
+ const lines = promptLinesBySession.get(sessionKey) ?? null;
7571
+ if (!useSectionBuilder) promptLinesBySession.delete(sessionKey);
7572
+ return lines;
7573
+ },
7574
+ extractionMaxTurnChars: options.capability.extractionMaxTurnChars,
7575
+ flushModel: options.capability.flushModel,
7576
+ configuredSearchBackend: options.capability.configuredSearchBackend,
7577
+ configuredQmdCommand: options.capability.configuredQmdCommand,
7578
+ searchTimeoutMs: options.recallTimeoutMs,
7579
+ healthTimeoutMs: options.recallTimeoutMs,
7580
+ now: options.now
7581
+ });
7582
+ if (promptInjectionEnabled) {
5941
7583
  const recallHandler = async (event, ctx) => {
5942
7584
  const query = recallQueryFrom(event);
5943
- if (query.trim().length < 5) return void 0;
5944
7585
  const sessionKey = sessionKeyFrom(event, ctx);
5945
- if (useSectionBuilder) promptLinesBySession.delete(sessionKey);
7586
+ if (cachePromptLines) promptLinesBySession.delete(sessionKey);
7587
+ if (query.trim().length < 5) return void 0;
7588
+ const promptDeadline = Date.now() + Math.min(options.hookTimeoutMs, options.recallTimeoutMs);
7589
+ const promptRemaining = () => promptDeadline - Date.now();
5946
7590
  try {
5947
7591
  if (options.shouldSkipRecall(sessionKey)) {
5948
- log2.debug(`delegate recall skipped: cron policy excludes ${sessionKey}`);
7592
+ log6.debug(`delegate recall skipped: cron policy excludes ${sessionKey}`);
5949
7593
  return void 0;
5950
7594
  }
5951
7595
  const runtimeAgent = ctx?.runtime?.agent;
5952
7596
  const agentId = (typeof ctx?.agentId === "string" ? ctx.agentId : void 0) ?? (typeof runtimeAgent?.id === "string" ? runtimeAgent.id : void 0) ?? "main";
5953
7597
  if (await options.resolveSessionDisabled(sessionKey, agentId)) {
5954
- log2.debug(`delegate recall skipped: session toggle disables memory for ${sessionKey}`);
7598
+ log6.debug(`delegate recall skipped: session toggle disables memory for ${sessionKey}`);
5955
7599
  return void 0;
5956
7600
  }
5957
7601
  const cwd = cwdFrom(event, ctx, options.cwd);
@@ -5966,14 +7610,21 @@ function registerDelegateRuntime(api, options) {
5966
7610
  target,
5967
7611
  options.serviceId,
5968
7612
  "/engram/v1/recall",
5969
- withNamespace(scopedNamespace, {
5970
- query,
5971
- sessionKey,
5972
- mode: "auto",
5973
- ...cwd ? { cwd } : {},
5974
- ...options.projectTag ? { projectTag: options.projectTag } : {}
5975
- }),
5976
- options.recallTimeoutMs
7613
+ await withNamespace(
7614
+ scopedNamespace,
7615
+ {
7616
+ query,
7617
+ sessionKey,
7618
+ mode: "auto",
7619
+ ...cwd ? { cwd } : {},
7620
+ ...options.projectTag ? { projectTag: options.projectTag } : {}
7621
+ },
7622
+ // ONE deadline for the whole hook: health resolution followed by
7623
+ // a full-timeout recall could otherwise run past `hookTimeoutMs`
7624
+ // and have the host abandon it with nothing injected.
7625
+ (explicit) => capability.resolveScopedNamespace(explicit, promptRemaining(), ["recall"])
7626
+ ),
7627
+ Math.max(1, promptRemaining())
5977
7628
  );
5978
7629
  const rawContext = response?.context;
5979
7630
  if (typeof rawContext !== "string" || rawContext.trim().length === 0) {
@@ -5985,13 +7636,13 @@ function registerDelegateRuntime(api, options) {
5985
7636
  });
5986
7637
  if (!rendered) return void 0;
5987
7638
  const prompt = rendered.prompt;
5988
- if (useSectionBuilder) {
7639
+ if (cachePromptLines) {
5989
7640
  promptLinesBySession.set(sessionKey, rendered.lines);
5990
7641
  return void 0;
5991
7642
  }
5992
7643
  return { prependSystemContext: prompt };
5993
7644
  } catch (err) {
5994
- log2.warn(`delegate recall failed: ${String(err)}`);
7645
+ log6.warn(`delegate recall failed: ${String(err)}`);
5995
7646
  return void 0;
5996
7647
  }
5997
7648
  };
@@ -6013,7 +7664,7 @@ function registerDelegateRuntime(api, options) {
6013
7664
  api.registerMemoryPromptSection(memoryBuildFn);
6014
7665
  }
6015
7666
  } else {
6016
- log2.info(
7667
+ log6.info(
6017
7668
  `[${options.serviceId}] bridge mode delegate: prompt injection disabled by hooks policy`
6018
7669
  );
6019
7670
  }
@@ -6032,6 +7683,8 @@ function registerDelegateRuntime(api, options) {
6032
7683
  );
6033
7684
  if (turn.length === 0) return;
6034
7685
  try {
7686
+ const observeDeadline = Date.now() + options.observeTimeoutMs;
7687
+ const observeRemaining = () => observeDeadline - Date.now();
6035
7688
  const cwd = cwdFrom(event, ctx, options.cwd);
6036
7689
  const scopedNamespace = await sessionNamespaceFrom(
6037
7690
  sessionKey,
@@ -6044,16 +7697,20 @@ function registerDelegateRuntime(api, options) {
6044
7697
  target,
6045
7698
  options.serviceId,
6046
7699
  "/engram/v1/observe",
6047
- withNamespace(scopedNamespace, {
6048
- sessionKey,
6049
- messages: turn,
6050
- ...cwd ? { cwd } : {},
6051
- ...options.projectTag ? { projectTag: options.projectTag } : {}
6052
- }),
6053
- options.observeTimeoutMs
7700
+ await withNamespace(
7701
+ scopedNamespace,
7702
+ {
7703
+ sessionKey,
7704
+ messages: turn,
7705
+ ...cwd ? { cwd } : {},
7706
+ ...options.projectTag ? { projectTag: options.projectTag } : {}
7707
+ },
7708
+ (explicit) => capability.resolveScopedNamespace(explicit, observeRemaining(), ["observe"])
7709
+ ),
7710
+ Math.max(1, observeRemaining())
6054
7711
  );
6055
7712
  } catch (err) {
6056
- log2.warn(`delegate observe failed: ${String(err)}`);
7713
+ log6.warn(`delegate observe failed: ${String(err)}`);
6057
7714
  }
6058
7715
  });
6059
7716
  let cachedBatchFlushSupport;
@@ -6104,9 +7761,10 @@ function registerDelegateRuntime(api, options) {
6104
7761
  try {
6105
7762
  const deadline = Date.now() + options.flushTimeoutMs;
6106
7763
  const remainingTimeout = () => Math.max(1, deadline - Date.now());
7764
+ const remainingBudget = () => deadline - Date.now();
6107
7765
  const sessionKey = lifecycleSessionKeyFrom(event, ctx);
6108
7766
  if (sessionKey === void 0) {
6109
- log2.warn("delegate flush skipped: lifecycle event has malformed session key");
7767
+ log6.warn("delegate flush skipped: lifecycle event has malformed session key");
6110
7768
  return false;
6111
7769
  }
6112
7770
  const namespaces = await lifecycleSessionNamespacesFrom(
@@ -6116,11 +7774,38 @@ function registerDelegateRuntime(api, options) {
6116
7774
  namespace,
6117
7775
  namespaceBindings
6118
7776
  );
6119
- const flushNamespace = (sessionNamespace) => postJson(
7777
+ try {
7778
+ await ingestFlushPlanNotes({
7779
+ target,
7780
+ serviceId: options.serviceId,
7781
+ workspaceDir: cwdFrom(event, ctx, options.capability.workspaceDir),
7782
+ sessionKey,
7783
+ // The session's CURRENT binding, which is the last entry of the
7784
+ // ordered history — `namespaces[0]` is where it started, so a
7785
+ // rebound session would file new notes under the previous tenant.
7786
+ namespace: namespaces.at(-1),
7787
+ // Re-read per chunk, not captured once: several posts must share
7788
+ // the flush's remaining budget rather than each taking it whole.
7789
+ remainingTimeoutMs: remainingBudget
7790
+ });
7791
+ } catch (err) {
7792
+ log6.warn(`delegate flush-plan ingestion failed: ${String(err)}`);
7793
+ }
7794
+ const flushNamespace = async (sessionNamespace) => postJson(
6120
7795
  target,
6121
7796
  options.serviceId,
6122
7797
  "/engram/v1/lcm/compaction/flush",
6123
- withNamespace(sessionNamespace, { sessionKey }),
7798
+ await withNamespace(
7799
+ sessionNamespace,
7800
+ { sessionKey },
7801
+ (explicit) => (
7802
+ // Inside the flush's SHARED deadline: a health probe started here
7803
+ // with its own full timeout would overrun the hook.
7804
+ capability.resolveScopedNamespace(explicit, remainingBudget(), [
7805
+ "lcm_compaction_flush"
7806
+ ])
7807
+ )
7808
+ ),
6124
7809
  remainingTimeout()
6125
7810
  );
6126
7811
  const flushIndividually = async () => {
@@ -6134,15 +7819,19 @@ function registerDelegateRuntime(api, options) {
6134
7819
  const response = await flushNamespace(namespaces[0]);
6135
7820
  return response !== null && response.flushed === true;
6136
7821
  }
7822
+ const requestNamespaces = await Promise.all(
7823
+ namespaces.map(
7824
+ async (sessionNamespace) => await capability.resolveScopedNamespace(sessionNamespace || void 0, remainingBudget(), [
7825
+ "lcm_compaction_flush"
7826
+ ]) ?? ""
7827
+ )
7828
+ );
6137
7829
  try {
6138
7830
  const response = await postJson(
6139
7831
  target,
6140
7832
  options.serviceId,
6141
7833
  "/engram/v1/lcm/compaction/flush",
6142
- {
6143
- sessionKey,
6144
- namespaces: namespaces.map((sessionNamespace) => sessionNamespace ?? "")
6145
- },
7834
+ { sessionKey, namespaces: requestNamespaces },
6146
7835
  remainingTimeout()
6147
7836
  );
6148
7837
  if (response === null) {
@@ -6151,9 +7840,9 @@ function registerDelegateRuntime(api, options) {
6151
7840
  }
6152
7841
  const responseNamespaces = response.namespaces;
6153
7842
  const responseResults = response.results;
6154
- const isBatchResponse = Array.isArray(responseNamespaces) && Array.isArray(responseResults) && responseNamespaces.length === namespaces.length && responseNamespaces.every(
6155
- (responseNamespace, index) => responseNamespace === (namespaces[index] ?? "")
6156
- ) && responseResults.length === namespaces.length;
7843
+ const isBatchResponse = Array.isArray(responseNamespaces) && Array.isArray(responseResults) && responseNamespaces.length === requestNamespaces.length && responseNamespaces.every(
7844
+ (responseNamespace, index) => responseNamespace === requestNamespaces[index]
7845
+ ) && responseResults.length === requestNamespaces.length;
6157
7846
  if (!isBatchResponse) {
6158
7847
  invalidateCachedBatchFlushSupport();
6159
7848
  return flushIndividually();
@@ -6170,7 +7859,7 @@ function registerDelegateRuntime(api, options) {
6170
7859
  }
6171
7860
  return flushIndividually();
6172
7861
  } catch (err) {
6173
- log2.warn(`delegate flush failed: ${String(err)}`);
7862
+ log6.warn(`delegate flush failed: ${String(err)}`);
6174
7863
  return false;
6175
7864
  }
6176
7865
  };
@@ -6180,12 +7869,12 @@ function registerDelegateRuntime(api, options) {
6180
7869
  api.on("before_reset", flushEndedSession);
6181
7870
  api.on("session_end", flushEndedSession);
6182
7871
  }
6183
- log2.info(
6184
- `[${options.serviceId}] bridge mode delegate: memory loop backed by daemon at ${target.host}:${target.port} (embedded orchestrator skipped; tools/CLI/surfaces stay daemon-side)`
7872
+ log6.info(
7873
+ `[${options.serviceId}] bridge mode delegate: memory loop backed by daemon at ${target.host}:${target.port} (embedded orchestrator skipped; tools/CLI stay daemon-side)`
6185
7874
  );
6186
7875
  }
6187
7876
  function activeDelegateAuthorizationOperations(options) {
6188
- return options.allowPromptInjection && options.recallBudgetChars !== 0 ? DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS : DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS.slice(1);
7877
+ return options.allowPromptInjection && options.recallBudgetChars !== 0 ? DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS : DEFAULT_DELEGATE_AUTHORIZATION_OPERATIONS.filter((operation) => operation !== "recall");
6189
7878
  }
6190
7879
  var delegateNamespaceMigrationChains = /* @__PURE__ */ new Map();
6191
7880
  var queueDelegateNamespaceMigration = (bindingPath, sessionKey, operation) => {
@@ -6211,7 +7900,7 @@ var queueDelegateNamespaceMigration = (bindingPath, sessionKey, operation) => {
6211
7900
  return run;
6212
7901
  };
6213
7902
  function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapterActive) {
6214
- const bindingPath = (pluginId) => path4.join(memoryDir, "state", "plugins", pluginId, "session-namespace-bindings.json");
7903
+ const bindingPath = (pluginId) => path8.join(memoryDir, "state", "plugins", pluginId, "session-namespace-bindings.json");
6215
7904
  const primaryPath = bindingPath(serviceId);
6216
7905
  const primary = createFileSessionNamespaceBindingStore(primaryPath);
6217
7906
  if (serviceId !== REMNIC_OPENCLAW_PLUGIN_ID) return primary;
@@ -6237,7 +7926,7 @@ function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapt
6237
7926
  return previous;
6238
7927
  } catch (err) {
6239
7928
  if (current.length > 0) {
6240
- log2.warn(
7929
+ log6.warn(
6241
7930
  `[${serviceId}] delegate legacy namespace read failed; using canonical bindings: ${String(err)}`
6242
7931
  );
6243
7932
  return [];
@@ -6252,7 +7941,7 @@ function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapt
6252
7941
  if (existing >= 0) merged.splice(existing, 1);
6253
7942
  merged.push(remembered);
6254
7943
  }
6255
- return merged.slice(-SESSION_NAMESPACE_BINDING_MAX_NAMESPACES);
7944
+ return merged.slice(-SESSION_NAMESPACE_BINDING_MAX_NAMESPACES2);
6256
7945
  };
6257
7946
  const persistNamespaceHistory = async (store, sessionKey, namespaces) => {
6258
7947
  if (store.replace !== void 0) {
@@ -6268,7 +7957,7 @@ function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapt
6268
7957
  try {
6269
7958
  await legacy.replace?.(sessionKey, []);
6270
7959
  } catch (err) {
6271
- log2.warn(`[${serviceId}] delegate legacy namespace cleanup failed: ${String(err)}`);
7960
+ log6.warn(`[${serviceId}] delegate legacy namespace cleanup failed: ${String(err)}`);
6272
7961
  }
6273
7962
  }
6274
7963
  rememberMigratedLegacySession(sessionKey);
@@ -6289,7 +7978,7 @@ function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapt
6289
7978
  await persistNamespaceHistory(primary, sessionKey, merged);
6290
7979
  await completeLegacyMigration(sessionKey);
6291
7980
  } catch (err) {
6292
- log2.warn(`[${serviceId}] delegate namespace migration failed: ${String(err)}`);
7981
+ log6.warn(`[${serviceId}] delegate namespace migration failed: ${String(err)}`);
6293
7982
  }
6294
7983
  return merged;
6295
7984
  });
@@ -6312,50 +8001,67 @@ function createDelegateNamespaceBindingStore(memoryDir, serviceId, isLegacyAdapt
6312
8001
  var delegateHookApiServices = /* @__PURE__ */ new WeakMap();
6313
8002
  var delegateActiveServiceIds = /* @__PURE__ */ new Set();
6314
8003
  var delegateEmbeddedFallbackApis = /* @__PURE__ */ new WeakSet();
8004
+ var delegateBoundApis = /* @__PURE__ */ new WeakSet();
6315
8005
  var delegateAuthorizationPreflightServices = /* @__PURE__ */ new WeakMap();
6316
8006
  function maybeRegisterDelegateRuntime(api, options, deps = { checkHealth: checkDaemonHealthSync }) {
6317
- let bridge;
6318
- try {
6319
- bridge = resolveBridgeMode(options.configBridgeMode);
6320
- } catch (err) {
6321
- log2.error(`${String(err)} \u2014 falling back to the embedded runtime`);
6322
- delegateEmbeddedFallbackApis.add(api);
6323
- return false;
6324
- }
6325
- if (delegateEmbeddedFallbackApis.has(api)) {
6326
- log2.debug(
6327
- `delegate register: ${options.serviceId} previously fell back to embedded on this api \u2014 staying embedded to avoid stacking memory paths`
6328
- );
6329
- return false;
6330
- }
6331
- if (bridge.mode !== "delegate") {
6332
- if (!options.passive) delegateEmbeddedFallbackApis.add(api);
6333
- return false;
6334
- }
6335
8007
  const boundServices = delegateHookApiServices.get(api);
6336
8008
  if (boundServices?.has(options.serviceId)) {
6337
- log2.debug(
8009
+ log6.debug(
6338
8010
  `delegate register: ${options.serviceId} already has hooks bound on this api \u2014 skipping duplicate registration`
6339
8011
  );
6340
8012
  return true;
6341
8013
  }
8014
+ if (delegateEmbeddedFallbackApis.has(api)) {
8015
+ log6.debug(
8016
+ `delegate register: ${options.serviceId} previously fell back to embedded on this api \u2014 staying embedded to avoid stacking memory paths`
8017
+ );
8018
+ return false;
8019
+ }
8020
+ let bridge;
6342
8021
  let bridgeHealthTimeoutMs;
6343
8022
  try {
6344
8023
  bridgeHealthTimeoutMs = parseOpenClawBridgeConfig({
6345
8024
  bridgeHealthTimeoutMs: options.bridgeHealthTimeoutMs
6346
8025
  }).healthTimeoutMs;
8026
+ bridge = resolveBridgeMode(options.configBridgeMode, {
8027
+ memoryDir: options.memoryDir,
8028
+ timeoutMs: bridgeHealthTimeoutMs,
8029
+ onSkip: (reason) => log6.info(`[${options.serviceId}] bridge mode auto: staying embedded \u2014 ${reason}`)
8030
+ });
6347
8031
  } catch (err) {
6348
- log2.error(`${String(err)} \u2014 falling back to the embedded runtime`);
6349
- delegateEmbeddedFallbackApis.add(api);
8032
+ const wantedDelegate = requestedDelegate(options.configBridgeMode);
8033
+ log6.error(
8034
+ wantedDelegate ? `${String(err)} \u2014 falling back to the embedded runtime` : `${String(err)} \u2014 the deployment is embedded, so this only affects delegate mode`
8035
+ );
8036
+ if (!options.passive) delegateEmbeddedFallbackApis.add(api);
6350
8037
  return false;
6351
8038
  }
6352
- if (!deps.checkHealth(
6353
- bridge.daemonHost,
6354
- bridge.daemonPort,
6355
- bridgeHealthTimeoutMs
6356
- )) {
8039
+ if (bridge.mode !== "delegate") {
8040
+ if (delegateBoundApis.has(api)) {
8041
+ log6.warn(
8042
+ `[${options.serviceId}] bridge mode resolved embedded, but a sibling service already bound delegate hooks on this api \u2014 reusing them instead of stacking an embedded runtime`
8043
+ );
8044
+ return true;
8045
+ }
8046
+ if (!options.passive) {
8047
+ delegateEmbeddedFallbackApis.add(api);
8048
+ if (resolveRequestedBridgeMode(options.configBridgeMode) === "auto") {
8049
+ log6.info(
8050
+ `[${options.serviceId}] bridge mode auto: embedded hooks are bound on this api \u2014 a daemon that starts later is picked up on the next gateway restart`
8051
+ );
8052
+ }
8053
+ }
8054
+ return false;
8055
+ }
8056
+ if (!bridge.healthVerified && !deps.checkHealth(bridge.daemonHost, bridge.daemonPort, bridgeHealthTimeoutMs)) {
8057
+ if (delegateBoundApis.has(api)) {
8058
+ log6.warn(
8059
+ `[${options.serviceId}] no healthy daemon at ${bridge.daemonHost}:${bridge.daemonPort}, but a sibling service already bound delegate hooks on this api \u2014 reusing them instead of stacking an embedded runtime`
8060
+ );
8061
+ return true;
8062
+ }
6357
8063
  delegateEmbeddedFallbackApis.add(api);
6358
- log2.error(
8064
+ log6.error(
6359
8065
  `bridge mode delegate requested but no healthy daemon at ${bridge.daemonHost}:${bridge.daemonPort} \u2014 falling back to the embedded runtime`
6360
8066
  );
6361
8067
  return false;
@@ -6365,18 +8071,15 @@ function maybeRegisterDelegateRuntime(api, options, deps = { checkHealth: checkD
6365
8071
  options.serviceId
6366
8072
  );
6367
8073
  delegateActiveServiceIds.add(options.serviceId);
8074
+ delegateBoundApis.add(api);
6368
8075
  }
6369
8076
  const toggleStore = options.sessionTogglesEnabled ? createFileToggleStore(
6370
- path4.join(options.memoryDir, "state", "plugins", options.serviceId, "session-toggles.json"),
8077
+ path8.join(options.memoryDir, "state", "plugins", options.serviceId, "session-toggles.json"),
6371
8078
  {
6372
- secondaryReadOnlyPath: options.respectBundledActiveMemoryToggle ? path4.join(options.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0
8079
+ secondaryReadOnlyPath: options.respectBundledActiveMemoryToggle ? path8.join(options.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0
6373
8080
  }
6374
8081
  ) : null;
6375
- const target = {
6376
- host: bridge.daemonHost,
6377
- port: bridge.daemonPort,
6378
- resolveAuthToken: loadDaemonAuth
6379
- };
8082
+ const target = daemonTargetFor(bridge);
6380
8083
  registerDelegateRuntime(api, {
6381
8084
  serviceId: options.serviceId,
6382
8085
  target,
@@ -6397,6 +8100,7 @@ function maybeRegisterDelegateRuntime(api, options, deps = { checkHealth: checkD
6397
8100
  cwd: options.cwd,
6398
8101
  projectTag: options.projectTag,
6399
8102
  flushOnResetEnabled: options.flushOnResetEnabled,
8103
+ capability: options.capability,
6400
8104
  recallTimeoutMs: 25e3,
6401
8105
  observeTimeoutMs: 12e4,
6402
8106
  flushTimeoutMs: 55e3
@@ -6414,16 +8118,16 @@ function maybeRegisterDelegateRuntime(api, options, deps = { checkHealth: checkD
6414
8118
  void probe(target, "", operations).then((result) => {
6415
8119
  if (result.state === "authorized") return;
6416
8120
  if (result.state === "unauthorized") {
6417
- log2.warn(
8121
+ log6.warn(
6418
8122
  `delegate authorization preflight rejected ${operationLabel} (${result.status}; token source: ${result.tokenSource}) \u2014 runtime remains active`
6419
8123
  );
6420
8124
  return;
6421
8125
  }
6422
- log2.warn(
8126
+ log6.warn(
6423
8127
  `delegate authorization preflight could not verify ${operationLabel} (token source: ${result.tokenSource}) \u2014 runtime remains active`
6424
8128
  );
6425
8129
  }).catch(() => {
6426
- log2.warn("delegate authorization preflight could not complete \u2014 runtime remains active");
8130
+ log6.warn("delegate authorization preflight could not complete \u2014 runtime remains active");
6427
8131
  });
6428
8132
  }
6429
8133
  return true;
@@ -6526,7 +8230,7 @@ function buildTurnFingerprint(input) {
6526
8230
  // ../../src/index.ts
6527
8231
  import { planRecallMode } from "@remnic/core/intent";
6528
8232
  import {
6529
- expandTildePath as expandTildePath2,
8233
+ expandTildePath as expandTildePath4,
6530
8234
  renderMemoryContextPrompt as renderSharedMemoryContextPrompt,
6531
8235
  resolveAgentAccessAuthToken,
6532
8236
  resolvePrincipal
@@ -6542,32 +8246,32 @@ __export(resolve_provider_secret_exports, {
6542
8246
  findGatewayRuntimeModules: () => findGatewayRuntimeModules
6543
8247
  });
6544
8248
  __reExport(resolve_provider_secret_exports, resolve_provider_secret_star);
6545
- import path5 from "path";
8249
+ import path9 from "path";
6546
8250
  import * as resolve_provider_secret_star from "@remnic/core/resolve-provider-secret";
6547
8251
  async function findGatewayRuntimeModules(filePrefix) {
6548
- const { existsSync, readFileSync, readdirSync, realpathSync } = await import("fs");
8252
+ const { existsSync, readFileSync, readdirSync, realpathSync: realpathSync2 } = await import("fs");
6549
8253
  const { createRequire: createRequire2 } = await import("module");
6550
8254
  const candidates = [];
6551
8255
  const isWithinRoot = (root, candidate) => {
6552
- const relative = path5.relative(root, candidate);
6553
- return relative.length === 0 || !relative.startsWith("..") && !path5.isAbsolute(relative);
8256
+ const relative = path9.relative(root, candidate);
8257
+ return relative.length === 0 || !relative.startsWith("..") && !path9.isAbsolute(relative);
6554
8258
  };
6555
8259
  let packageRoot;
6556
8260
  try {
6557
8261
  const req = createRequire2(import.meta.url);
6558
- const openclawEntrypoint = realpathSync(req.resolve("openclaw"));
6559
- let currentDir = path5.dirname(openclawEntrypoint);
8262
+ const openclawEntrypoint = realpathSync2(req.resolve("openclaw"));
8263
+ let currentDir = path9.dirname(openclawEntrypoint);
6560
8264
  while (true) {
6561
- const packageJsonPath = path5.join(currentDir, "package.json");
8265
+ const packageJsonPath = path9.join(currentDir, "package.json");
6562
8266
  if (existsSync(packageJsonPath)) {
6563
8267
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
6564
8268
  if (packageJson.name !== "openclaw") {
6565
8269
  return [];
6566
8270
  }
6567
- packageRoot = realpathSync(currentDir);
8271
+ packageRoot = realpathSync2(currentDir);
6568
8272
  break;
6569
8273
  }
6570
- const parent = path5.dirname(currentDir);
8274
+ const parent = path9.dirname(currentDir);
6571
8275
  if (parent === currentDir) {
6572
8276
  return [];
6573
8277
  }
@@ -6577,14 +8281,14 @@ async function findGatewayRuntimeModules(filePrefix) {
6577
8281
  return [];
6578
8282
  }
6579
8283
  try {
6580
- const distDir = realpathSync(path5.join(packageRoot, "dist"));
8284
+ const distDir = realpathSync2(path9.join(packageRoot, "dist"));
6581
8285
  if (!isWithinRoot(packageRoot, distDir)) {
6582
8286
  return [];
6583
8287
  }
6584
8288
  const files = readdirSync(distDir);
6585
8289
  for (const f of files) {
6586
8290
  if (f.startsWith(filePrefix) && f.endsWith(".js")) {
6587
- const candidate = realpathSync(path5.join(distDir, f));
8291
+ const candidate = realpathSync2(path9.join(distDir, f));
6588
8292
  if (isWithinRoot(packageRoot, candidate) && isWithinRoot(distDir, candidate)) {
6589
8293
  candidates.push(candidate);
6590
8294
  }
@@ -6666,28 +8370,25 @@ var NODE_FS_MODULE_ID = ["node", "fs"].join(":");
6666
8370
  var NODE_FS_PROMISES_MODULE_ID = ["node", "fs/promises"].join(":");
6667
8371
  var READ_FILE_SYNC_FIELD = ["read", "File", "Sync"].join("");
6668
8372
  var EXISTS_SYNC_FIELD = ["exists", "Sync"].join("");
6669
- function isMemoryArtifactPath(p) {
6670
- return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(p);
6671
- }
6672
8373
  function readTextFileNow(filePath) {
6673
8374
  const nodeRequire = createRequire(import.meta.url);
6674
- const fs2 = nodeRequire(NODE_FS_MODULE_ID);
6675
- const reader = fs2[READ_FILE_SYNC_FIELD];
8375
+ const fs4 = nodeRequire(NODE_FS_MODULE_ID);
8376
+ const reader = fs4[READ_FILE_SYNC_FIELD];
6676
8377
  return reader(filePath, "utf-8");
6677
8378
  }
6678
8379
  function fileExistsNow(filePath) {
6679
8380
  const nodeRequire = createRequire(import.meta.url);
6680
- const fs2 = nodeRequire(NODE_FS_MODULE_ID);
6681
- const exists = fs2[EXISTS_SYNC_FIELD];
8381
+ const fs4 = nodeRequire(NODE_FS_MODULE_ID);
8382
+ const exists = fs4[EXISTS_SYNC_FIELD];
6682
8383
  return exists(filePath);
6683
8384
  }
6684
8385
  async function readTextFileLater(filePath) {
6685
- const fs2 = await import(NODE_FS_PROMISES_MODULE_ID);
6686
- return fs2.readFile(filePath, "utf-8");
8386
+ const fs4 = await import(NODE_FS_PROMISES_MODULE_ID);
8387
+ return fs4.readFile(filePath, "utf-8");
6687
8388
  }
6688
8389
  async function writeTextFileLater(filePath, data) {
6689
- const fs2 = await import(NODE_FS_PROMISES_MODULE_ID);
6690
- await fs2.writeFile(filePath, data, "utf-8");
8390
+ const fs4 = await import(NODE_FS_PROMISES_MODULE_ID);
8391
+ await fs4.writeFile(filePath, data, "utf-8");
6691
8392
  }
6692
8393
  function isRecordLike(value) {
6693
8394
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6806,8 +8507,8 @@ function reconcileHourlySummaryCronRouting(existing, cfg, opts) {
6806
8507
  return { changed: true, job };
6807
8508
  }
6808
8509
  async function realPathLater(filePath) {
6809
- const fs2 = await import(NODE_FS_PROMISES_MODULE_ID);
6810
- return fs2.realpath(filePath);
8510
+ const fs4 = await import(NODE_FS_PROMISES_MODULE_ID);
8511
+ return fs4.realpath(filePath);
6811
8512
  }
6812
8513
  var SECRET_REF_RESOLVER_RETRY_BACKOFF_MS = 6e4;
6813
8514
  var SECRET_REF_RESOLVER_EXPORT_NAMES = [
@@ -6928,9 +8629,9 @@ function buildServiceKeys(serviceId) {
6928
8629
  function resolveOpenClawConfigFilePath() {
6929
8630
  const explicitConfigPath = readEnvVar("OPENCLAW_CONFIG_PATH") || readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH");
6930
8631
  if (explicitConfigPath && explicitConfigPath.length > 0) {
6931
- return expandTildePath2(explicitConfigPath);
8632
+ return expandTildePath4(explicitConfigPath);
6932
8633
  }
6933
- return path6.join(resolveHomeDir2(), ".openclaw", "openclaw.json");
8634
+ return path10.join(resolveHomeDir3(), ".openclaw", "openclaw.json");
6934
8635
  }
6935
8636
  function coerceRawConfigBoolean(value) {
6936
8637
  if (typeof value === "boolean") return value;
@@ -6977,7 +8678,7 @@ function readPluginHooksPolicy(apiConfig, pluginId) {
6977
8678
  }
6978
8679
  async function maybeRegisterLiveConnectorCron(orchestrator) {
6979
8680
  if (!hasEnabledLiveConnectorConfig(orchestrator.config.connectors)) return;
6980
- const jobsPath = path6.join(resolveHomeDir2(), ".openclaw", "cron", "jobs.json");
8681
+ const jobsPath = path10.join(resolveHomeDir3(), ".openclaw", "cron", "jobs.json");
6981
8682
  try {
6982
8683
  if (!fileExistsNow(jobsPath)) {
6983
8684
  logger_exports.log.debug("live connectors cron: jobs.json not found, skipping auto-register");
@@ -7205,9 +8906,20 @@ function registerOpenClawHostEmbeddingProvider(params) {
7205
8906
  );
7206
8907
  return unregister;
7207
8908
  }
8909
+ function getOpenClawRuntimeAgent(api) {
8910
+ if (!("runtime" in api)) return void 0;
8911
+ const runtime = api.runtime;
8912
+ if (typeof runtime !== "object" || runtime === null || !("agent" in runtime)) return void 0;
8913
+ const agent = runtime.agent;
8914
+ return typeof agent === "object" && agent !== null ? agent : void 0;
8915
+ }
7208
8916
  function getOpenClawRuntimeWorkspaceDir(api) {
7209
- const runtimeWorkspaceDir = api.runtime?.agent?.workspaceDir;
7210
- return typeof runtimeWorkspaceDir === "string" && runtimeWorkspaceDir.length > 0 ? runtimeWorkspaceDir : void 0;
8917
+ const workspaceDir = getOpenClawRuntimeAgent(api)?.workspaceDir;
8918
+ return typeof workspaceDir === "string" && workspaceDir.length > 0 ? workspaceDir : void 0;
8919
+ }
8920
+ function getOpenClawRuntimeAgentId(api) {
8921
+ const agentId = getOpenClawRuntimeAgent(api)?.id;
8922
+ return typeof agentId === "string" && agentId.length > 0 ? agentId : void 0;
7211
8923
  }
7212
8924
  function stableOpenClawConfigSignature(value, seen = /* @__PURE__ */ new WeakSet()) {
7213
8925
  if (value === null) return "null";
@@ -7439,7 +9151,19 @@ var pluginDefinition = {
7439
9151
  hookTimeoutMs: cfg.initGateTimeoutMs,
7440
9152
  shouldSkipRecall: (sk) => shouldSkipRecallForSession(sk, cfg),
7441
9153
  cwd: getOpenClawRuntimeWorkspaceDir(api),
7442
- flushOnResetEnabled: cfg.flushOnResetEnabled
9154
+ flushOnResetEnabled: cfg.flushOnResetEnabled,
9155
+ // Memory-slot capability inputs. Mirrors the embedded derivation: the
9156
+ // registration-time runtime agent owns this memory, and QMD is the
9157
+ // backend only when it is both selected and enabled.
9158
+ capability: {
9159
+ memoryDir: cfg.memoryDir,
9160
+ workspaceDir: getOpenClawRuntimeWorkspaceDir(api) ?? cfg.workspaceDir ?? defaultWorkspaceDir(),
9161
+ agentIds: [getOpenClawRuntimeAgentId(api) ?? "generalist"],
9162
+ extractionMaxTurnChars: cfg.extractionMaxTurnChars,
9163
+ flushModel: typeof cfg.summaryModel === "string" && cfg.summaryModel.length > 0 ? cfg.summaryModel : cfg.taskModelChain?.primary,
9164
+ configuredSearchBackend: (cfg.searchBackend ?? "qmd") === "qmd" && cfg.qmdEnabled !== false ? "qmd" : "builtin",
9165
+ configuredQmdCommand: typeof cfg.qmdPath === "string" && cfg.qmdPath.trim().length > 0 ? cfg.qmdPath.trim() : "qmd"
9166
+ }
7443
9167
  });
7444
9168
  if (delegateHandled) return;
7445
9169
  const existing = globalThis[keys.ORCHESTRATOR];
@@ -7521,9 +9245,9 @@ var pluginDefinition = {
7521
9245
  emitLegacyTools: cfg.emitLegacyTools
7522
9246
  });
7523
9247
  globalThis[keys.ACCESS_HTTP_SERVER] = accessHttpServer;
7524
- const pluginStateDir = path6.join(cfg.memoryDir, "state", "plugins", serviceId);
7525
- const togglePrimaryPath = path6.join(pluginStateDir, "session-toggles.json");
7526
- const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path6.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
9248
+ const pluginStateDir = path10.join(cfg.memoryDir, "state", "plugins", serviceId);
9249
+ const togglePrimaryPath = path10.join(pluginStateDir, "session-toggles.json");
9250
+ const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path10.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
7527
9251
  const sessionToggleStore = createFileToggleStore2(togglePrimaryPath, {
7528
9252
  secondaryReadOnlyPath: toggleSecondaryPath
7529
9253
  });
@@ -7545,11 +9269,11 @@ var pluginDefinition = {
7545
9269
  }
7546
9270
  function resolveDreamJournalPath(runtimeWorkspaceDir) {
7547
9271
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
7548
- return path6.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path6.join(workspaceRoot, cfg.dreaming.journalPath);
9272
+ return path10.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path10.join(workspaceRoot, cfg.dreaming.journalPath);
7549
9273
  }
7550
9274
  function resolveHeartbeatJournalPath(runtimeWorkspaceDir) {
7551
9275
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
7552
- return path6.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path6.join(workspaceRoot, cfg.heartbeat.journalPath);
9276
+ return path10.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path10.join(workspaceRoot, cfg.heartbeat.journalPath);
7553
9277
  }
7554
9278
  const existingFlushPlanProcessingChains = globalThis[keys.FLUSH_PLAN_PROCESSING_CHAINS];
7555
9279
  const flushPlanProcessingChains = existingFlushPlanProcessingChains instanceof Map ? existingFlushPlanProcessingChains : /* @__PURE__ */ new Map();
@@ -7826,7 +9550,7 @@ Keep the reflection grounded in the evidence below.
7826
9550
  timeoutMs: cfg.activeRecallTimeoutMs,
7827
9551
  cacheTtlMs: cfg.activeRecallCacheTtlMs,
7828
9552
  persistTranscripts: cfg.activeRecallPersistTranscripts,
7829
- transcriptDir: path6.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path6.join(pluginStateDir, cfg.activeRecallTranscriptDir),
9553
+ transcriptDir: path10.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path10.join(pluginStateDir, cfg.activeRecallTranscriptDir),
7830
9554
  entityGraphDepth: cfg.activeRecallEntityGraphDepth,
7831
9555
  includeCausalTrajectories: cfg.activeRecallIncludeCausalTrajectories,
7832
9556
  includeDaySummary: cfg.activeRecallIncludeDaySummary,
@@ -8759,94 +10483,15 @@ Keep the reflection grounded in the evidence below.
8759
10483
  const key = params?.sessionKey ?? "default";
8760
10484
  return consumePromptMemoryLines2(key);
8761
10485
  };
8762
- const runtimeAgent = api.runtime?.agent;
8763
- const runtimeAgentId = typeof runtimeAgent?.id === "string" && runtimeAgent.id.length > 0 ? runtimeAgent.id : void 0;
8764
- const capabilityAgentIds = runtimeAgentId ? [runtimeAgentId] : ["generalist"];
8765
- const capabilityWorkspaceDir = (typeof runtimeAgent?.workspaceDir === "string" && runtimeAgent.workspaceDir.length > 0 ? runtimeAgent.workspaceDir : void 0) ?? orchestrator.config.workspaceDir ?? defaultWorkspaceDir();
10486
+ const capabilityAgentIds = [getOpenClawRuntimeAgentId(api) ?? "generalist"];
10487
+ const capabilityWorkspaceDir = getOpenClawRuntimeWorkspaceDir(api) ?? orchestrator.config.workspaceDir ?? defaultWorkspaceDir();
8766
10488
  const remnicUsesQmd = (orchestrator.config.searchBackend ?? "qmd") === "qmd" && orchestrator.config.qmdEnabled !== false;
8767
10489
  const remnicQmdCommand = typeof orchestrator.config.qmdPath === "string" && orchestrator.config.qmdPath.trim().length > 0 ? orchestrator.config.qmdPath.trim() : "qmd";
8768
- const readAllowedRoots = [
8769
- orchestrator.config.memoryDir,
8770
- capabilityWorkspaceDir ? path6.join(capabilityWorkspaceDir, "memory") : void 0
8771
- ].filter((root) => typeof root === "string" && root.length > 0);
8772
- const canonicalizeRootForContainment = async (rawPath) => {
8773
- const resolved = path6.resolve(rawPath);
8774
- try {
8775
- return path6.normalize(await realPathLater(resolved));
8776
- } catch {
8777
- return path6.normalize(resolved);
8778
- }
8779
- };
8780
- const canonicalizeForRead = async (rawPath) => {
8781
- const resolved = path6.resolve(rawPath);
8782
- const real = await realPathLater(resolved);
8783
- return path6.normalize(real);
8784
- };
8785
- const readAllowedCanonicalRootsPromise = Promise.all(
8786
- readAllowedRoots.map((root) => canonicalizeRootForContainment(root))
8787
- );
8788
- const isWithinAllowedRoot = async (candidatePath) => {
8789
- let canonicalCandidatePath;
8790
- try {
8791
- canonicalCandidatePath = await canonicalizeForRead(candidatePath);
8792
- } catch {
8793
- return false;
8794
- }
8795
- const canonicalRoots = await readAllowedCanonicalRootsPromise;
8796
- return canonicalRoots.some((root) => {
8797
- const relative = path6.relative(root, canonicalCandidatePath);
8798
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
8799
- });
8800
- };
8801
- const normalizeWorkspacePath = (rawPath) => {
8802
- if (!rawPath || typeof rawPath !== "string") return "memory";
8803
- const resolved = path6.isAbsolute(rawPath) ? path6.resolve(rawPath) : path6.resolve(capabilityWorkspaceDir, rawPath);
8804
- const relative = path6.relative(capabilityWorkspaceDir, resolved);
8805
- return relative && !relative.startsWith("..") && !path6.isAbsolute(relative) ? relative : rawPath;
8806
- };
8807
- const relativizeToMemoryRoot = (rawPath) => {
8808
- if (!rawPath || typeof rawPath !== "string") return "memory";
8809
- const resolved = path6.isAbsolute(rawPath) ? path6.resolve(rawPath) : path6.resolve(capabilityWorkspaceDir, rawPath);
8810
- for (const root of readAllowedRoots) {
8811
- const relative = path6.relative(root, resolved);
8812
- if (relative !== "" && !relative.startsWith("..") && !path6.isAbsolute(relative)) {
8813
- return relative;
8814
- }
8815
- }
8816
- return normalizeWorkspacePath(rawPath);
8817
- };
8818
- const resolveReadablePath = async (requestedPath) => {
8819
- const candidateAbsolutePaths = path6.isAbsolute(requestedPath) ? [path6.resolve(requestedPath)] : readAllowedRoots.map((root) => path6.resolve(root, requestedPath));
8820
- let canonicalPath;
8821
- let lastError;
8822
- for (const absolutePath of candidateAbsolutePaths) {
8823
- try {
8824
- canonicalPath = await canonicalizeForRead(absolutePath);
8825
- break;
8826
- } catch (err) {
8827
- lastError = err;
8828
- }
8829
- }
8830
- if (canonicalPath === void 0) {
8831
- throw new Error(
8832
- `memory read rejected (path unresolvable): ${requestedPath}`
8833
- );
8834
- }
8835
- const canonicalRoots = await readAllowedCanonicalRootsPromise;
8836
- const contained = canonicalRoots.some((root) => {
8837
- const relative = path6.relative(root, canonicalPath);
8838
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
8839
- });
8840
- if (!contained) {
8841
- throw new Error(`memory read outside allowed roots: ${requestedPath}`);
8842
- }
8843
- if (!canonicalPath.toLowerCase().endsWith(".md")) {
8844
- throw new Error(
8845
- `memory read restricted to .md files: ${requestedPath}`
8846
- );
8847
- }
8848
- return canonicalPath;
8849
- };
10490
+ const readScope = createMemoryReadScope({
10491
+ memoryDir: orchestrator.config.memoryDir,
10492
+ workspaceDir: capabilityWorkspaceDir,
10493
+ realpath: realPathLater
10494
+ });
8850
10495
  const remnicMemoryRuntime = {
8851
10496
  async getMemorySearchManager(_params) {
8852
10497
  return {
@@ -8867,17 +10512,8 @@ Keep the reflection grounded in the evidence below.
8867
10512
  }).map((result, index) => {
8868
10513
  const candidate = result;
8869
10514
  const rawPath = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : `memory-${index + 1}`;
8870
- const absolutePath = path6.isAbsolute(rawPath) ? path6.resolve(rawPath) : (() => {
8871
- for (const root of readAllowedRoots) {
8872
- const candidateAbs = path6.resolve(root, rawPath);
8873
- const relative = path6.relative(root, candidateAbs);
8874
- if (!relative.startsWith("..") && !path6.isAbsolute(relative)) {
8875
- return candidateAbs;
8876
- }
8877
- }
8878
- return path6.resolve(capabilityWorkspaceDir, rawPath);
8879
- })();
8880
- const normalizedPath = relativizeToMemoryRoot(rawPath);
10515
+ const absolutePath = readScope.absolutize(rawPath);
10516
+ const normalizedPath = readScope.relativizeToMemoryRoot(rawPath);
8881
10517
  const startLine = typeof candidate.startLine === "number" && Number.isFinite(candidate.startLine) ? Math.max(1, Math.floor(candidate.startLine)) : 1;
8882
10518
  const endLine = typeof candidate.endLine === "number" && Number.isFinite(candidate.endLine) ? Math.max(startLine, Math.floor(candidate.endLine)) : startLine;
8883
10519
  return {
@@ -8886,7 +10522,7 @@ Keep the reflection grounded in the evidence below.
8886
10522
  endLine,
8887
10523
  score: typeof candidate.score === "number" && Number.isFinite(candidate.score) ? candidate.score : 0,
8888
10524
  snippet: typeof candidate.snippet === "string" ? candidate.snippet : typeof candidate.text === "string" ? candidate.text : "",
8889
- source: normalizedPath.includes("sessions/") ? "sessions" : "memory",
10525
+ source: isSessionsMemoryPath(normalizedPath) ? "sessions" : "memory",
8890
10526
  citation: normalizedPath
8891
10527
  };
8892
10528
  }).filter(
@@ -8894,8 +10530,8 @@ Keep the reflection grounded in the evidence below.
8894
10530
  );
8895
10531
  },
8896
10532
  async readFile(params) {
8897
- const requestedPath = normalizeWorkspacePath(params.relPath);
8898
- const absolutePath = await resolveReadablePath(params.relPath);
10533
+ const requestedPath = readScope.normalizeWorkspacePath(params.relPath);
10534
+ const absolutePath = await readScope.resolveReadablePath(params.relPath);
8899
10535
  const text = await readTextFileLater(absolutePath);
8900
10536
  const allLines = text.split(/\r?\n/);
8901
10537
  const from = typeof params.from === "number" ? Math.max(1, Math.floor(params.from)) : 1;
@@ -8975,19 +10611,13 @@ Keep the reflection grounded in the evidence below.
8975
10611
  async closeAllMemorySearchManagers() {
8976
10612
  }
8977
10613
  };
8978
- const remnicMemoryFlushPlanResolver = () => {
8979
- const maxTurnChars = typeof cfg.extractionMaxTurnChars === "number" && Number.isFinite(cfg.extractionMaxTurnChars) ? Math.max(1e3, Math.floor(cfg.extractionMaxTurnChars)) : 8e3;
8980
- const flushModel = typeof cfg.summaryModel === "string" && cfg.summaryModel.length > 0 ? cfg.summaryModel : cfg.taskModelChain?.primary;
8981
- return {
8982
- softThresholdTokens: 24e3,
8983
- forceFlushTranscriptBytes: Math.max(16384, maxTurnChars * 4),
8984
- reserveTokensFloor: 2e3,
8985
- ...flushModel ? { model: flushModel } : {},
8986
- prompt: "Flush the recent OpenClaw transcript into Remnic memory by appending to the allowed flush-plan file only. Preserve durable user preferences, project facts, decisions, corrections, and commitments. Ignore runtime metadata, credentials, and transient command noise.",
8987
- systemPrompt: "You are Remnic's memory flush planner. Read the transcript and append concise durable memory notes to the file the write tool allows. Do not create files, directories, or dated paths; use only the allowed flush-plan file. Ignore runtime metadata, credentials, transient command noise, and content that is not worth remembering.",
8988
- relativePath: ["state", "plugins", serviceId, "flush-plan.md"].join("/")
8989
- };
8990
- };
10614
+ const remnicMemoryFlushPlanResolver = () => buildMemoryFlushPlan({
10615
+ serviceId,
10616
+ extractionMaxTurnChars: cfg.extractionMaxTurnChars,
10617
+ // `summaryModel` already resolves explicit summary/base model →
10618
+ // gateway task-chain primary → "" (gateway mode).
10619
+ flushModel: typeof cfg.summaryModel === "string" && cfg.summaryModel.length > 0 ? cfg.summaryModel : cfg.taskModelChain?.primary
10620
+ });
8991
10621
  const memoryCapability = {
8992
10622
  // Include the promptBuilder so runtimes that treat unified capability
8993
10623
  // registration as authoritative (SDK >=2026.4.5) continue to inject
@@ -9454,7 +11084,7 @@ Keep the reflection grounded in the evidence below.
9454
11084
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
9455
11085
  );
9456
11086
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
9457
- const signalPath = path6.join(
11087
+ const signalPath = path10.join(
9458
11088
  workspaceDir,
9459
11089
  `.compaction-reset-signal-${safeSessionKey}`
9460
11090
  );
@@ -9673,7 +11303,7 @@ Keep the reflection grounded in the evidence below.
9673
11303
  }
9674
11304
  async function ensureHourlySummaryCron(api2) {
9675
11305
  const jobId = "engram-hourly-summary";
9676
- const cronFilePath = path6.join(
11306
+ const cronFilePath = path10.join(
9677
11307
  os.homedir(),
9678
11308
  ".openclaw",
9679
11309
  "cron",
@@ -9732,32 +11362,32 @@ Keep the reflection grounded in the evidence below.
9732
11362
  };
9733
11363
  const normalizeCorpusPath = (value) => value.trim().replace(/\\/g, "/").replace(/^\.\//, "");
9734
11364
  const pathIsInside = (root, candidate) => {
9735
- const relative = path6.relative(root, candidate);
9736
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
11365
+ const relative = path10.relative(root, candidate);
11366
+ return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
9737
11367
  };
9738
11368
  const corpusPathCandidates = (rawPath, storageDir) => {
9739
11369
  const candidates = /* @__PURE__ */ new Set();
9740
11370
  const trimmed = rawPath.trim();
9741
11371
  if (!trimmed) return [];
9742
11372
  candidates.add(normalizeCorpusPath(trimmed));
9743
- if (path6.isAbsolute(trimmed)) {
9744
- const absolutePath = path6.resolve(trimmed);
9745
- const absoluteStorageDir = path6.resolve(storageDir);
11373
+ if (path10.isAbsolute(trimmed)) {
11374
+ const absolutePath = path10.resolve(trimmed);
11375
+ const absoluteStorageDir = path10.resolve(storageDir);
9746
11376
  if (pathIsInside(absoluteStorageDir, absolutePath)) {
9747
- candidates.add(normalizeCorpusPath(path6.relative(absoluteStorageDir, absolutePath)));
11377
+ candidates.add(normalizeCorpusPath(path10.relative(absoluteStorageDir, absolutePath)));
9748
11378
  }
9749
11379
  }
9750
- candidates.add(path6.basename(trimmed));
11380
+ candidates.add(path10.basename(trimmed));
9751
11381
  return [...candidates].filter((candidate) => candidate.length > 0);
9752
11382
  };
9753
11383
  const displayCorpusPath = (rawPath, storageDir) => {
9754
11384
  const trimmed = rawPath.trim();
9755
11385
  if (!trimmed) return "";
9756
- if (path6.isAbsolute(trimmed)) {
9757
- const absolutePath = path6.resolve(trimmed);
9758
- const absoluteStorageDir = path6.resolve(storageDir);
11386
+ if (path10.isAbsolute(trimmed)) {
11387
+ const absolutePath = path10.resolve(trimmed);
11388
+ const absoluteStorageDir = path10.resolve(storageDir);
9759
11389
  if (pathIsInside(absoluteStorageDir, absolutePath)) {
9760
- return normalizeCorpusPath(path6.relative(absoluteStorageDir, absolutePath));
11390
+ return normalizeCorpusPath(path10.relative(absoluteStorageDir, absolutePath));
9761
11391
  }
9762
11392
  }
9763
11393
  return normalizeCorpusPath(trimmed);
@@ -10267,9 +11897,9 @@ function truncateMetadataValue(value, maxChars) {
10267
11897
  return value.length <= maxChars ? value : value.slice(0, maxChars);
10268
11898
  }
10269
11899
  async function resolveFlushPlanProcessingChainKey(workspaceRoot) {
10270
- const lexicalRoot = path6.resolve(workspaceRoot);
11900
+ const lexicalRoot = path10.resolve(workspaceRoot);
10271
11901
  try {
10272
- return path6.resolve(await realPathLater(lexicalRoot));
11902
+ return path10.resolve(await realPathLater(lexicalRoot));
10273
11903
  } catch {
10274
11904
  return lexicalRoot;
10275
11905
  }
@@ -10280,12 +11910,15 @@ export {
10280
11910
  checkDaemonHealth,
10281
11911
  src_default as default,
10282
11912
  detectBridgeMode,
11913
+ detectDaemonBridgeMode,
10283
11914
  embedWithOpenClawProvider,
10284
11915
  listRemnicPublicArtifacts,
10285
11916
  export_loadDaySummaryPrompt as loadDaySummaryPrompt,
10286
11917
  loadHourlySummaryCronJobsData,
10287
11918
  loadOpenClawMemoryEmbeddingSdk,
10288
11919
  parseHourlySummaryCronJobsData,
11920
+ readDaemonMemoryDirSync,
10289
11921
  reconcileHourlySummaryCronRouting,
11922
+ resolveBridgeMode,
10290
11923
  selectOpenClawMemoryEmbeddingSdk
10291
11924
  };