@remnic/plugin-openclaw 9.46.1 → 9.47.0
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 +2143 -510
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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
|
|
5350
|
-
import
|
|
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
|
-
|
|
5354
|
-
|
|
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
|
|
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 (!
|
|
5429
|
+
if (!settled) finish(false);
|
|
5416
5430
|
});
|
|
5417
5431
|
req.on("timeout", () => {
|
|
5418
5432
|
req.destroy();
|
|
5419
|
-
if (!
|
|
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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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
|
|
5440
|
-
|
|
5441
|
-
[
|
|
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
|
|
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 ? [
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
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
|
|
6141
|
+
function fileExists2(filePath) {
|
|
5464
6142
|
try {
|
|
5465
|
-
return
|
|
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
|
-
|
|
5473
|
-
|
|
6150
|
+
path6.join(resolveHomeDir2(), ".remnic", "server.pid"),
|
|
6151
|
+
path6.join(resolveHomeDir2(), ".engram", "server.pid")
|
|
5474
6152
|
]) {
|
|
5475
6153
|
try {
|
|
5476
|
-
const pid = parseInt(
|
|
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
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5500
|
-
|
|
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:
|
|
5513
|
-
fallbackPath:
|
|
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 ===
|
|
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
|
|
5530
|
-
|
|
5531
|
-
return
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5564
|
-
const
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
}
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
const
|
|
5580
|
-
const
|
|
5581
|
-
|
|
5582
|
-
|
|
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
|
|
6488
|
+
function resolveRequestedBridgeMode(configBridgeMode) {
|
|
5596
6489
|
const envMode = readCompatEnv("REMNIC_BRIDGE_MODE", "ENGRAM_BRIDGE_MODE")?.toLowerCase();
|
|
5597
|
-
|
|
5598
|
-
if (envMode
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
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:
|
|
5634
|
-
{ path:
|
|
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 (!
|
|
6568
|
+
if (!fs3.existsSync(tokenStore.path)) continue;
|
|
5638
6569
|
try {
|
|
5639
|
-
const store = JSON.parse(
|
|
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
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
const raw = JSON.parse(
|
|
5656
|
-
const
|
|
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
|
-
|
|
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
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 ===
|
|
6155
|
-
(responseNamespace, index) => responseNamespace ===
|
|
6156
|
-
) && responseResults.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
|
-
|
|
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
|
-
|
|
6184
|
-
`[${options.serviceId}] bridge mode delegate: memory loop backed by daemon at ${target.host}:${target.port} (embedded orchestrator skipped; tools/CLI
|
|
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.
|
|
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) =>
|
|
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
|
-
|
|
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(-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6349
|
-
|
|
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 (
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8077
|
+
path8.join(options.memoryDir, "state", "plugins", options.serviceId, "session-toggles.json"),
|
|
6371
8078
|
{
|
|
6372
|
-
secondaryReadOnlyPath: options.respectBundledActiveMemoryToggle ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
6553
|
-
return relative.length === 0 || !relative.startsWith("..") && !
|
|
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 =
|
|
6559
|
-
let currentDir =
|
|
8262
|
+
const openclawEntrypoint = realpathSync2(req.resolve("openclaw"));
|
|
8263
|
+
let currentDir = path9.dirname(openclawEntrypoint);
|
|
6560
8264
|
while (true) {
|
|
6561
|
-
const packageJsonPath =
|
|
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 =
|
|
8271
|
+
packageRoot = realpathSync2(currentDir);
|
|
6568
8272
|
break;
|
|
6569
8273
|
}
|
|
6570
|
-
const parent =
|
|
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 =
|
|
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 =
|
|
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
|
|
6675
|
-
const reader =
|
|
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
|
|
6681
|
-
const exists =
|
|
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
|
|
6686
|
-
return
|
|
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
|
|
6690
|
-
await
|
|
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
|
|
6810
|
-
return
|
|
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
|
|
8632
|
+
return expandTildePath4(explicitConfigPath);
|
|
6932
8633
|
}
|
|
6933
|
-
return
|
|
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 =
|
|
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
|
|
7210
|
-
return typeof
|
|
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 =
|
|
7525
|
-
const togglePrimaryPath =
|
|
7526
|
-
const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ?
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
8763
|
-
const
|
|
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
|
|
8769
|
-
orchestrator.config.memoryDir,
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
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 =
|
|
8871
|
-
|
|
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
|
|
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
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
9736
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
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 (
|
|
9744
|
-
const absolutePath =
|
|
9745
|
-
const absoluteStorageDir =
|
|
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(
|
|
11377
|
+
candidates.add(normalizeCorpusPath(path10.relative(absoluteStorageDir, absolutePath)));
|
|
9748
11378
|
}
|
|
9749
11379
|
}
|
|
9750
|
-
candidates.add(
|
|
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 (
|
|
9757
|
-
const absolutePath =
|
|
9758
|
-
const absoluteStorageDir =
|
|
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(
|
|
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 =
|
|
11900
|
+
const lexicalRoot = path10.resolve(workspaceRoot);
|
|
10271
11901
|
try {
|
|
10272
|
-
return
|
|
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
|
};
|