dsh-mnemon-source-memory-spaces 0.5.18 → 0.5.20
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/lib/{process-DIK9NnTg.js → adapter-D-BnWkko.js} +4 -4
- package/lib/client.js +276 -143
- package/lib/contracts.d.ts +59 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +676 -141
- package/lib/provider-sdk.js +1 -1
- package/lib/types/client/api.d.ts +7 -1
- package/lib/types/contracts.d.ts +58 -0
- package/package.json +4 -4
- package/presentation/locales.json +22 -2
- package/presentation/page.module.css +7 -0
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { findMnemonCommand, isMnemonExecutable, resolveMnemonInvocation } from "./native-cli.js";
|
|
2
2
|
import { CATEGORIES as CATEGORIES$1, EDGE_TYPES, INTENTS as INTENTS$1, MNEMON_EMBEDDING_PROTOCOLS, SOURCES as SOURCES$1, normalizeEntityKey } from "./contracts.js";
|
|
3
3
|
import { a as MemoryProviderCatalog, i as EMPTY_MEMORY_PROVIDER_CATALOG, n as PrivateMemorySpaceProviderHost, r as MemoryProviderAdapterRegistry, t as createMemorySpaceProviderPlugin } from "./plugin-DlsNeZbQ.js";
|
|
4
|
-
import {
|
|
4
|
+
import { i as runProcess, r as ProcessError } from "./adapter-D-BnWkko.js";
|
|
5
5
|
import { r as defineMemorySpaceProvider } from "./definitions-DfmOqJjR.js";
|
|
6
6
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -43,6 +43,7 @@ function modelSpaceCatalog(catalog) {
|
|
|
43
43
|
function modelStatus(status) {
|
|
44
44
|
const active = status.memoryBodies.filter((body) => body.active && body.providerEnabled !== false);
|
|
45
45
|
const unhealthy = active.filter((body) => !body.healthy);
|
|
46
|
+
const summed = active.filter((body) => body.healthy && body.stats !== void 0);
|
|
46
47
|
const relevantProviders = status.providerServices?.filter((provider) => provider.enabled || provider.configured || provider.memoryBodyCount > 0 || provider.status === "unhealthy") ?? [];
|
|
47
48
|
const providers = relevantProviders.slice(0, 16);
|
|
48
49
|
const providerReady = status.commandFound === true || (status.providerServices ?? []).some((provider) => provider.enabled && provider.configured);
|
|
@@ -73,6 +74,8 @@ function modelStatus(status) {
|
|
|
73
74
|
})),
|
|
74
75
|
omittedProviderCount: Math.max(0, relevantProviders.length - providers.length),
|
|
75
76
|
...status.stats === void 0 ? {} : { aggregate: {
|
|
77
|
+
scope: "Sums over the active, healthy Memory Spaces whose Provider reports statistics (spaces); the others add nothing. totalInsights counts live memories, and deletedInsights the forgotten ones a Provider keeps, as Mnemon Native does.",
|
|
78
|
+
spaces: summed.length,
|
|
76
79
|
totalInsights: status.stats.totalInsights,
|
|
77
80
|
deletedInsights: status.stats.deletedInsights,
|
|
78
81
|
edgeCount: status.stats.edgeCount,
|
|
@@ -103,9 +106,40 @@ const NATIVE_REGISTRY_VERSION = 1;
|
|
|
103
106
|
const PROVIDER_REGISTRY_VERSION = 4;
|
|
104
107
|
const ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
105
108
|
const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]{0,127}$/u;
|
|
109
|
+
const DISCONNECTS_VERSION = 1;
|
|
110
|
+
/** Settings whose value can name a namespace: a bank, workspace, user, tag, URI or folder. */
|
|
111
|
+
const IDENTITY_INPUTS = /* @__PURE__ */ new Set([
|
|
112
|
+
"text",
|
|
113
|
+
"url",
|
|
114
|
+
"path"
|
|
115
|
+
]);
|
|
106
116
|
function storedProviderId(value) {
|
|
107
117
|
return typeof value === "string" && PROVIDER_ID_PATTERN.test(value);
|
|
108
118
|
}
|
|
119
|
+
/** Disconnected namespaces as a file holds them: sound entries only, newest last. */
|
|
120
|
+
function storedDisconnects(value) {
|
|
121
|
+
if (!Array.isArray(value)) return [];
|
|
122
|
+
return value.flatMap((item) => {
|
|
123
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return [];
|
|
124
|
+
const fields = item;
|
|
125
|
+
const externalId = typeof fields.externalId === "string" && fields.externalId.trim() !== "" && fields.externalId.trim().length <= 2e3 ? fields.externalId.trim() : void 0;
|
|
126
|
+
const connection = storedProviderConnection(fields.connection);
|
|
127
|
+
const entry = {
|
|
128
|
+
...externalId === void 0 ? {} : { externalId },
|
|
129
|
+
...Object.keys(connection).length === 0 ? {} : { connection }
|
|
130
|
+
};
|
|
131
|
+
return Object.keys(entry).length === 0 ? [] : [entry];
|
|
132
|
+
}).slice(-1e3);
|
|
133
|
+
}
|
|
134
|
+
/** JSON with every object's keys in order, so two equal connections compare equal. */
|
|
135
|
+
function canonicalJson(value) {
|
|
136
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
137
|
+
if (value !== null && typeof value === "object") {
|
|
138
|
+
const fields = value;
|
|
139
|
+
return `{${Object.keys(fields).filter((key) => fields[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(fields[key])}`).join(",")}}`;
|
|
140
|
+
}
|
|
141
|
+
return JSON.stringify(value) ?? "null";
|
|
142
|
+
}
|
|
109
143
|
function storedProviderConnection(value) {
|
|
110
144
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
111
145
|
return Object.fromEntries(Object.entries(value).filter((entry) => {
|
|
@@ -216,6 +250,8 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
216
250
|
directory;
|
|
217
251
|
registryPath;
|
|
218
252
|
providerRegistryPath;
|
|
253
|
+
/** The namespaces a person disconnected, which asking a Provider again leaves out (#373). */
|
|
254
|
+
disconnectsPath;
|
|
219
255
|
refreshing = false;
|
|
220
256
|
state;
|
|
221
257
|
constructor(runner, persistent = true, now = () => /* @__PURE__ */ new Date(), providerCatalog = EMPTY_MEMORY_PROVIDER_CATALOG, sharedState) {
|
|
@@ -226,11 +262,13 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
226
262
|
this.state = sharedState ?? {
|
|
227
263
|
spaces: [],
|
|
228
264
|
services: {},
|
|
229
|
-
serviceEnabled: {}
|
|
265
|
+
serviceEnabled: {},
|
|
266
|
+
disconnected: {}
|
|
230
267
|
};
|
|
231
268
|
this.directory = join(runner.effectiveDataDir(), "data");
|
|
232
269
|
this.registryPath = join(this.directory, ".dsh-memory-bodies.json");
|
|
233
270
|
this.providerRegistryPath = join(runner.effectiveDataDir(), "state", "memory-providers.json");
|
|
271
|
+
this.disconnectsPath = join(runner.effectiveDataDir(), "state", "memory-provider-disconnects.json");
|
|
234
272
|
if (sharedState === void 0) this.reload();
|
|
235
273
|
}
|
|
236
274
|
get spaces() {
|
|
@@ -251,6 +289,12 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
251
289
|
set serviceEnabled(value) {
|
|
252
290
|
this.state.serviceEnabled = value;
|
|
253
291
|
}
|
|
292
|
+
get disconnected() {
|
|
293
|
+
return this.state.disconnected;
|
|
294
|
+
}
|
|
295
|
+
set disconnected(value) {
|
|
296
|
+
this.state.disconnected = value;
|
|
297
|
+
}
|
|
254
298
|
/** One generation-local descriptor view over the same persistent authority. */
|
|
255
299
|
withProviderCatalog(providerCatalog) {
|
|
256
300
|
return new MemorySpaceRegistry(this.runner, this.persistent, this.now, providerCatalog, this.state);
|
|
@@ -400,8 +444,181 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
400
444
|
this.serviceEnabled[providerId] = true;
|
|
401
445
|
this.spaces = [...this.spaces.filter((body) => body.providerId !== providerId), ...projections];
|
|
402
446
|
this.save();
|
|
447
|
+
this.forgetDisconnected(providerId, this.reportedNamespaces(providerId, normalizedService, discovered));
|
|
403
448
|
return this.providerServices().items.find((item) => item.providerId === providerId);
|
|
404
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Mount the namespaces a Provider reports that this directory has never held, as asking a
|
|
452
|
+
* Provider again finds them (#373). A space already mounted is left exactly as it is, and so is
|
|
453
|
+
* a namespace a person disconnected. A new space starts inactive: recall reads it only once a
|
|
454
|
+
* person turns it on. A space counts as a reported namespace when it has its externalId, or the
|
|
455
|
+
* settings that name it (see `holds`): a space made by hand for the same bank, or one whose
|
|
456
|
+
* externalId the Provider now writes differently. A Provider switched off, or saved with other
|
|
457
|
+
* settings since `service` was read, gets nothing: that save decides. Nothing is written when
|
|
458
|
+
* nothing is new.
|
|
459
|
+
*/
|
|
460
|
+
mountDiscovered(providerId, service, discovered) {
|
|
461
|
+
this.refreshIfChanged();
|
|
462
|
+
if (this.isNative(providerId)) throw new Error("Mnemon Native Stores are discovered from disk");
|
|
463
|
+
const current = this.services[providerId];
|
|
464
|
+
if (current === void 0 || !this.providerServiceEnabled(providerId) || canonicalJson(current) !== canonicalJson(this.providerCatalog.normalizeService(providerId, service))) return {
|
|
465
|
+
added: [],
|
|
466
|
+
disconnected: 0
|
|
467
|
+
};
|
|
468
|
+
const held = this.spaces.filter((body) => body.providerId === providerId).map((body) => ({
|
|
469
|
+
...body.externalId === void 0 ? {} : { externalId: body.externalId },
|
|
470
|
+
...body.connection === void 0 ? {} : { connection: body.connection }
|
|
471
|
+
}));
|
|
472
|
+
const disconnected = this.disconnected[providerId] ?? [];
|
|
473
|
+
const reservedIds = new Set(this.spaces.map((body) => body.id));
|
|
474
|
+
const timestamp = this.now().toISOString();
|
|
475
|
+
const added = [];
|
|
476
|
+
let stillDisconnected = 0;
|
|
477
|
+
for (const reported of this.reportedNamespaces(providerId, current, discovered)) {
|
|
478
|
+
if (held.some((body) => this.holds(providerId, current, body, reported)) || added.some((body) => body.externalId === reported.externalId)) continue;
|
|
479
|
+
if (disconnected.some((entry) => this.holds(providerId, current, entry, reported))) {
|
|
480
|
+
stillDisconnected += 1;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const { externalId, connection, candidate } = reported;
|
|
484
|
+
let id = validateMemorySpaceId(`${providerId}-${createHash("sha256").update(externalId).digest("hex").slice(0, 24)}`);
|
|
485
|
+
let suffix = 1;
|
|
486
|
+
while (reservedIds.has(id)) {
|
|
487
|
+
id = validateMemorySpaceId(`${providerId}-${createHash("sha256").update(`${externalId}:${suffix}`).digest("hex").slice(0, 24)}`);
|
|
488
|
+
suffix += 1;
|
|
489
|
+
}
|
|
490
|
+
const metadata = providerProjectionMetadata(this.providerCatalog, providerId, candidate);
|
|
491
|
+
added.push({
|
|
492
|
+
id,
|
|
493
|
+
externalId,
|
|
494
|
+
name: metadata.name,
|
|
495
|
+
description: metadata.description,
|
|
496
|
+
metadataSource: "provider",
|
|
497
|
+
active: false,
|
|
498
|
+
providerId,
|
|
499
|
+
connection,
|
|
500
|
+
createdAt: timestamp,
|
|
501
|
+
updatedAt: timestamp
|
|
502
|
+
});
|
|
503
|
+
reservedIds.add(id);
|
|
504
|
+
}
|
|
505
|
+
if (added.length > 0) {
|
|
506
|
+
this.spaces = [...this.spaces, ...added];
|
|
507
|
+
this.save();
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
added: added.map((body) => this.view(body)),
|
|
511
|
+
disconnected: stillDisconnected
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
/** The namespaces in one Provider answer this directory could hold; the others are left out. */
|
|
515
|
+
reportedNamespaces(providerId, service, discovered) {
|
|
516
|
+
const reported = discovered.flatMap((candidate) => {
|
|
517
|
+
try {
|
|
518
|
+
const externalId = requiredText(candidate.externalId, "provider externalId", 2e3);
|
|
519
|
+
const connection = this.providerCatalog.normalizeMemory(providerId, candidate.connection);
|
|
520
|
+
this.providerCatalog.normalize(providerId, {
|
|
521
|
+
...service,
|
|
522
|
+
...connection
|
|
523
|
+
});
|
|
524
|
+
const identity = this.namespaceIdentity(providerId, connection);
|
|
525
|
+
const whole = identity === void 0 ? this.wholeConnection(providerId, service, connection) : void 0;
|
|
526
|
+
return [{
|
|
527
|
+
candidate,
|
|
528
|
+
externalId,
|
|
529
|
+
connection,
|
|
530
|
+
...identity === void 0 ? {} : { identity },
|
|
531
|
+
...whole === void 0 ? {} : { whole },
|
|
532
|
+
distinct: false
|
|
533
|
+
}];
|
|
534
|
+
} catch {
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
const signature = (namespace) => namespace.identity !== void 0 ? `identity:${canonicalJson(namespace.identity)}` : namespace.whole === void 0 ? void 0 : `connection:${namespace.whole}`;
|
|
539
|
+
const counts = /* @__PURE__ */ new Map();
|
|
540
|
+
for (const namespace of reported) {
|
|
541
|
+
const key = signature(namespace);
|
|
542
|
+
if (key !== void 0) counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
543
|
+
}
|
|
544
|
+
for (const namespace of reported) {
|
|
545
|
+
const key = signature(namespace);
|
|
546
|
+
namespace.distinct = key !== void 0 && counts.get(key) === 1;
|
|
547
|
+
}
|
|
548
|
+
return reported;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* What names a namespace in a space's own settings: each text, address or path set for it. A
|
|
552
|
+
* wildcard (`*`) names nothing, and neither does a choice, a switch, a number such as a recall
|
|
553
|
+
* budget or a trust floor, or a secret.
|
|
554
|
+
*/
|
|
555
|
+
namespaceIdentity(providerId, connection) {
|
|
556
|
+
if (connection === void 0) return void 0;
|
|
557
|
+
const identity = {};
|
|
558
|
+
for (const field of this.providerCatalog.memoryFields(providerId)) {
|
|
559
|
+
if (!IDENTITY_INPUTS.has(field.input)) continue;
|
|
560
|
+
const value = connection[field.key];
|
|
561
|
+
if (typeof value !== "string") continue;
|
|
562
|
+
const text = value.trim();
|
|
563
|
+
if (text === "" || text === "*") continue;
|
|
564
|
+
identity[field.key] = text;
|
|
565
|
+
}
|
|
566
|
+
return Object.keys(identity).length === 0 ? void 0 : identity;
|
|
567
|
+
}
|
|
568
|
+
/** A whole connection, defaults filled in; one whose own part is empty compares by externalId alone. */
|
|
569
|
+
wholeConnection(providerId, service, memory) {
|
|
570
|
+
if (memory === void 0 || Object.keys(memory).length === 0) return void 0;
|
|
571
|
+
try {
|
|
572
|
+
return canonicalJson(this.providerCatalog.normalize(providerId, {
|
|
573
|
+
...service,
|
|
574
|
+
...memory
|
|
575
|
+
}));
|
|
576
|
+
} catch {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Whether `held`, a mounted space or a namespace a person disconnected, already reads the
|
|
582
|
+
* namespace a Provider reported: the same externalId, or every setting that names the namespace,
|
|
583
|
+
* whatever else differs; a `*` held reads every value. A hand-made Hindsight space for bank
|
|
584
|
+
* `team` with another recall budget reads bank `team`; a Honcho space for one peer of workspace
|
|
585
|
+
* `w1` reads workspace `w1`. Asking again only adds what nothing here reads yet.
|
|
586
|
+
*/
|
|
587
|
+
holds(providerId, service, held, reported) {
|
|
588
|
+
if (held.externalId !== void 0 && held.externalId === reported.externalId) return true;
|
|
589
|
+
if (!reported.distinct || held.connection === void 0) return false;
|
|
590
|
+
if (reported.identity !== void 0) {
|
|
591
|
+
const connection = held.connection;
|
|
592
|
+
return Object.entries(reported.identity).every(([key, value]) => {
|
|
593
|
+
const setting = connection[key];
|
|
594
|
+
return typeof setting === "string" && (setting.trim() === value || setting.trim() === "*");
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return reported.whole !== void 0 && this.wholeConnection(providerId, service, held.connection) === reported.whole;
|
|
598
|
+
}
|
|
599
|
+
/** Remember a namespace a person disconnected, so asking its Provider again leaves it out. */
|
|
600
|
+
rememberDisconnected(providerId, body) {
|
|
601
|
+
const connection = body.connection === void 0 ? {} : this.providerCatalog.publicScoped(providerId, "memory", body.connection).settings;
|
|
602
|
+
const entry = {
|
|
603
|
+
...body.externalId === void 0 ? {} : { externalId: body.externalId },
|
|
604
|
+
...Object.keys(connection).length === 0 ? {} : { connection }
|
|
605
|
+
};
|
|
606
|
+
if (Object.keys(entry).length === 0) return;
|
|
607
|
+
const key = canonicalJson(entry);
|
|
608
|
+
this.disconnected[providerId] = [...(this.disconnected[providerId] ?? []).filter((item) => canonicalJson(item) !== key), entry].slice(-1e3);
|
|
609
|
+
this.saveDisconnects();
|
|
610
|
+
}
|
|
611
|
+
/** Forget the disconnected namespaces a save of the Provider's settings mounted again. */
|
|
612
|
+
forgetDisconnected(providerId, reported) {
|
|
613
|
+
const remembered = this.disconnected[providerId];
|
|
614
|
+
if (remembered === void 0) return;
|
|
615
|
+
const service = this.services[providerId] ?? {};
|
|
616
|
+
const remaining = remembered.filter((entry) => !reported.some((namespace) => this.holds(providerId, service, entry, namespace)));
|
|
617
|
+
if (remaining.length === remembered.length) return;
|
|
618
|
+
if (remaining.length === 0) delete this.disconnected[providerId];
|
|
619
|
+
else this.disconnected[providerId] = remaining;
|
|
620
|
+
this.saveDisconnects();
|
|
621
|
+
}
|
|
405
622
|
placementCandidates(request) {
|
|
406
623
|
return this.providerCatalog.providers.map((descriptor) => {
|
|
407
624
|
const providerTypeId = descriptor.typeId ?? descriptor.id;
|
|
@@ -556,8 +773,10 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
556
773
|
async remove(id, signal) {
|
|
557
774
|
const body = this.get(id);
|
|
558
775
|
if (!this.isNative(body.provider.id)) {
|
|
776
|
+
const stored = this.spaces.find((entry) => entry.id === body.id);
|
|
559
777
|
this.spaces = this.spaces.filter((entry) => entry.id !== body.id);
|
|
560
778
|
this.save();
|
|
779
|
+
if (stored !== void 0) this.rememberDisconnected(body.provider.id, stored);
|
|
561
780
|
return body;
|
|
562
781
|
}
|
|
563
782
|
const nativeStoreIds = this.nativeStoreIds();
|
|
@@ -621,6 +840,7 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
621
840
|
this.spaces = [];
|
|
622
841
|
this.services = {};
|
|
623
842
|
this.serviceEnabled = {};
|
|
843
|
+
this.disconnected = {};
|
|
624
844
|
this.loadAndReconcile();
|
|
625
845
|
this.state.diskRevision = this.diskRevision();
|
|
626
846
|
} finally {
|
|
@@ -629,7 +849,11 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
629
849
|
}
|
|
630
850
|
diskRevision() {
|
|
631
851
|
if (!this.persistent) return "ephemeral";
|
|
632
|
-
return [
|
|
852
|
+
return [
|
|
853
|
+
this.registryPath,
|
|
854
|
+
this.providerRegistryPath,
|
|
855
|
+
this.disconnectsPath
|
|
856
|
+
].map((path) => {
|
|
633
857
|
try {
|
|
634
858
|
const stat = statSync(path, { bigint: true });
|
|
635
859
|
return `${stat.ino}:${stat.mtimeNs}:${stat.size}`;
|
|
@@ -748,6 +972,13 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
748
972
|
}));
|
|
749
973
|
}
|
|
750
974
|
} catch {}
|
|
975
|
+
if (this.persistent && existsSync(this.disconnectsPath)) try {
|
|
976
|
+
const parsed = JSON.parse(readFileSync(this.disconnectsPath, "utf8"));
|
|
977
|
+
if (parsed.version === DISCONNECTS_VERSION && typeof parsed.providers === "object" && parsed.providers !== null) for (const [providerId, entries] of Object.entries(parsed.providers)) {
|
|
978
|
+
const remembered = storedDisconnects(entries);
|
|
979
|
+
if (storedProviderId(providerId) && remembered.length > 0) this.disconnected[providerId] = remembered;
|
|
980
|
+
}
|
|
981
|
+
} catch {}
|
|
751
982
|
const retainedBodies = this.spaces.filter((body) => !this.providerCatalog.has(body.providerId) || this.isNative(body.providerId) || this.providerServiceEnabled(body.providerId));
|
|
752
983
|
if (retainedBodies.length !== this.spaces.length) {
|
|
753
984
|
this.spaces = retainedBodies;
|
|
@@ -849,6 +1080,22 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
849
1080
|
});
|
|
850
1081
|
this.state.diskRevision = this.diskRevision();
|
|
851
1082
|
}
|
|
1083
|
+
saveDisconnects() {
|
|
1084
|
+
if (!this.persistent) return;
|
|
1085
|
+
const providers = Object.fromEntries(Object.entries(this.disconnected).filter(([, entries]) => entries !== void 0 && entries.length > 0));
|
|
1086
|
+
if (Object.keys(providers).length === 0) rmSync(this.disconnectsPath, { force: true });
|
|
1087
|
+
else {
|
|
1088
|
+
mkdirSync(dirname(this.disconnectsPath), {
|
|
1089
|
+
recursive: true,
|
|
1090
|
+
mode: 448
|
|
1091
|
+
});
|
|
1092
|
+
this.writeRegistry(this.disconnectsPath, {
|
|
1093
|
+
version: DISCONNECTS_VERSION,
|
|
1094
|
+
providers
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
this.state.diskRevision = this.diskRevision();
|
|
1098
|
+
}
|
|
852
1099
|
writeRegistry(path, file) {
|
|
853
1100
|
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
|
854
1101
|
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}\n`, {
|
|
@@ -859,6 +1106,132 @@ var MemorySpaceRegistry = class MemorySpaceRegistry {
|
|
|
859
1106
|
}
|
|
860
1107
|
};
|
|
861
1108
|
//#endregion
|
|
1109
|
+
//#region src/runner.ts
|
|
1110
|
+
var MnemonCliError = class extends Error {
|
|
1111
|
+
reason;
|
|
1112
|
+
constructor(message, reason) {
|
|
1113
|
+
super(message);
|
|
1114
|
+
this.reason = reason;
|
|
1115
|
+
this.name = "MnemonCliError";
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
const EMBEDDING_ENVIRONMENT_KEYS = /* @__PURE__ */ new Set([
|
|
1119
|
+
"MNEMON_EMBED_ENDPOINT",
|
|
1120
|
+
"MNEMON_EMBED_MODEL",
|
|
1121
|
+
"MNEMON_EMBED_API_KEY",
|
|
1122
|
+
"MNEMON_EMBED_PROTOCOL"
|
|
1123
|
+
]);
|
|
1124
|
+
/**
|
|
1125
|
+
* Explain a CLI call that produced no result. Only a failed launch means the
|
|
1126
|
+
* executable is missing or unusable, so only that case carries the install
|
|
1127
|
+
* hint; a timeout, a cancellation or an oversized output names its own cause.
|
|
1128
|
+
*/
|
|
1129
|
+
function processFailureMessage(error, args) {
|
|
1130
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1131
|
+
const reason = error instanceof ProcessError ? error.reason : "launch";
|
|
1132
|
+
const command = args.find((arg) => !arg.startsWith("-"));
|
|
1133
|
+
if (reason === "output-limit") return `mnemon ${command ?? "command"} stopped: ${detail.replace(/^mnemon /u, "")}`;
|
|
1134
|
+
if (reason === "timeout" && command !== void 0) return detail.replace(/^mnemon /u, `mnemon ${command} `);
|
|
1135
|
+
if (reason !== "launch") return detail;
|
|
1136
|
+
return `${detail}. ${process.platform === "win32" ? "Install the official Mnemon Windows release, ensure mnemon.exe is on PATH or under %LOCALAPPDATA%\\Programs\\mnemon, or set MNEMON_CLI_PATH or mnemon.cliPath to its absolute path." : "Install Mnemon and ensure \"mnemon\" is on PATH, or set MNEMON_CLI_PATH or mnemon.cliPath."}`;
|
|
1137
|
+
}
|
|
1138
|
+
/** Preserve the Host environment while making saved embedding overrides authoritative. */
|
|
1139
|
+
function processEnvironment(config) {
|
|
1140
|
+
if (!config.embedding.enabled) return void 0;
|
|
1141
|
+
return {
|
|
1142
|
+
...Object.fromEntries(Object.entries(process.env).filter(([key]) => !EMBEDDING_ENVIRONMENT_KEYS.has(key.toUpperCase()))),
|
|
1143
|
+
MNEMON_EMBED_ENDPOINT: config.embedding.endpoint,
|
|
1144
|
+
MNEMON_EMBED_MODEL: config.embedding.model,
|
|
1145
|
+
MNEMON_EMBED_API_KEY: config.embedding.apiKey,
|
|
1146
|
+
...config.embedding.protocol === "auto" ? {} : { MNEMON_EMBED_PROTOCOL: config.embedding.protocol }
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function createRunner(config, processRunner = runProcess) {
|
|
1150
|
+
const currentCommand = () => findMnemonCommand(config) ?? config.cliPath ?? "mnemon";
|
|
1151
|
+
const globalArgs = (store) => {
|
|
1152
|
+
const args = ["--data-dir", effectiveDataDir()];
|
|
1153
|
+
if (store !== void 0) args.push("--store", store);
|
|
1154
|
+
else if (config.store !== void 0) args.push("--store", config.store);
|
|
1155
|
+
return args;
|
|
1156
|
+
};
|
|
1157
|
+
const effectiveDataDir = () => config.dataDir;
|
|
1158
|
+
const persistedStore = () => {
|
|
1159
|
+
const active = join(effectiveDataDir(), "active");
|
|
1160
|
+
if (existsSync(active)) try {
|
|
1161
|
+
const value = readFileSync(active, "utf8").trim();
|
|
1162
|
+
if (/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) return value;
|
|
1163
|
+
} catch {}
|
|
1164
|
+
return "default";
|
|
1165
|
+
};
|
|
1166
|
+
const launch = async (args, options = {}) => {
|
|
1167
|
+
if (options.signal?.aborted === true) throw new MnemonCliError(`mnemon command aborted: ${String(options.signal.reason ?? "cancelled")}`, "aborted");
|
|
1168
|
+
if (options.database !== void 0 && !existsSync(options.database)) throw new MnemonCliError(`mnemon ${args.find((arg) => !arg.startsWith("-")) ?? "command"}: no database at ${options.database}`, "no-database");
|
|
1169
|
+
const argv = options.globalFlags === false ? [...args] : [...globalArgs(options.store), ...args];
|
|
1170
|
+
const environment = processEnvironment(config);
|
|
1171
|
+
const processOptions = {
|
|
1172
|
+
timeoutMs: options.timeoutMs ?? config.timeoutMs,
|
|
1173
|
+
...environment === void 0 ? {} : { env: environment },
|
|
1174
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
1175
|
+
...options.maxOutputBytes === void 0 ? {} : { maxOutputBytes: options.maxOutputBytes }
|
|
1176
|
+
};
|
|
1177
|
+
let result;
|
|
1178
|
+
try {
|
|
1179
|
+
const invocation = resolveMnemonInvocation(currentCommand(), argv, environment);
|
|
1180
|
+
result = await processRunner(invocation.command, invocation.args, {
|
|
1181
|
+
...processOptions,
|
|
1182
|
+
...invocation.env === void 0 ? {} : { env: invocation.env }
|
|
1183
|
+
});
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
throw new MnemonCliError(processFailureMessage(error, args), error instanceof ProcessError ? error.reason : "launch");
|
|
1186
|
+
}
|
|
1187
|
+
if (result.exitCode !== 0) {
|
|
1188
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
1189
|
+
throw new MnemonCliError(`mnemon ${args.join(" ")} exited ${String(result.exitCode)}: ${detail}`);
|
|
1190
|
+
}
|
|
1191
|
+
return result.stdout;
|
|
1192
|
+
};
|
|
1193
|
+
const execute = (args, options = {}) => {
|
|
1194
|
+
return withMemoryStorageLock(effectiveDataDir(), () => launch(args, options));
|
|
1195
|
+
};
|
|
1196
|
+
return {
|
|
1197
|
+
get command() {
|
|
1198
|
+
return currentCommand();
|
|
1199
|
+
},
|
|
1200
|
+
get commandFound() {
|
|
1201
|
+
const found = findMnemonCommand(config);
|
|
1202
|
+
return found !== void 0 && isMnemonExecutable(found);
|
|
1203
|
+
},
|
|
1204
|
+
async runJson(args, options) {
|
|
1205
|
+
const stdout = await execute(args, options);
|
|
1206
|
+
try {
|
|
1207
|
+
return JSON.parse(stdout);
|
|
1208
|
+
} catch {
|
|
1209
|
+
throw new MnemonCliError(`mnemon ${args.join(" ")} returned invalid JSON`);
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1212
|
+
runText: execute,
|
|
1213
|
+
runTextBatch(commands) {
|
|
1214
|
+
return withMemoryStorageLock(effectiveDataDir(), async () => {
|
|
1215
|
+
const outputs = [];
|
|
1216
|
+
for (const command of commands) outputs.push(await launch(command.args, command.options));
|
|
1217
|
+
return outputs;
|
|
1218
|
+
});
|
|
1219
|
+
},
|
|
1220
|
+
effectiveDataDir() {
|
|
1221
|
+
return effectiveDataDir();
|
|
1222
|
+
},
|
|
1223
|
+
persistedStore() {
|
|
1224
|
+
return persistedStore();
|
|
1225
|
+
},
|
|
1226
|
+
effectiveStore() {
|
|
1227
|
+
if (config.store !== void 0) return config.store;
|
|
1228
|
+
const fromEnvironment = process.env.MNEMON_STORE?.trim();
|
|
1229
|
+
if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
|
|
1230
|
+
return persistedStore();
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
//#endregion
|
|
862
1235
|
//#region src/provider-placement.ts
|
|
863
1236
|
const CAPABILITY_LABELS = {
|
|
864
1237
|
graph: "typed graph",
|
|
@@ -1300,6 +1673,25 @@ function number(value) {
|
|
|
1300
1673
|
const ENTITY_SELECTION_STATUS_REUSE_MS = 1e4;
|
|
1301
1674
|
/** How long an index that statistics cannot check is reused; a write through this Source still drops it. */
|
|
1302
1675
|
const ENTITY_INDEX_UNCHECKED_REUSE_MS = 1e4;
|
|
1676
|
+
/** How long one Provider may take to list its namespaces when they are asked for again. */
|
|
1677
|
+
const MEMORY_SPACES_REDISCOVERY_PROVIDER_TIMEOUT_MS = 3e4;
|
|
1678
|
+
/**
|
|
1679
|
+
* Rediscovery per data directory, shared by every generation in this process: the pass now
|
|
1680
|
+
* running, which a second request joins, and the last one, which the Memory Spaces page shows.
|
|
1681
|
+
*/
|
|
1682
|
+
const rediscoveries = /* @__PURE__ */ new Map();
|
|
1683
|
+
/** Settle with `work`, or reject once `signal` aborts, whichever comes first. */
|
|
1684
|
+
function untilAborted(work, signal) {
|
|
1685
|
+
if (signal.aborted) {
|
|
1686
|
+
work.catch(() => {});
|
|
1687
|
+
return Promise.reject(signal.reason);
|
|
1688
|
+
}
|
|
1689
|
+
return new Promise((resolve, reject) => {
|
|
1690
|
+
const abort = () => reject(signal.reason);
|
|
1691
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1692
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1303
1695
|
function entityName(entity) {
|
|
1304
1696
|
const name = entity.trim();
|
|
1305
1697
|
if (name.length > 200) throw new Error("entity is too long (max 200 characters)");
|
|
@@ -1486,6 +1878,41 @@ function mutationResultCompletion(result) {
|
|
|
1486
1878
|
function mutationResultCommitted(result) {
|
|
1487
1879
|
return mutationResultCompletion(result) === "committed";
|
|
1488
1880
|
}
|
|
1881
|
+
/** One `mnemon embed --status` answer, strictly validated. */
|
|
1882
|
+
function embeddingCounts(value) {
|
|
1883
|
+
const output = record(value);
|
|
1884
|
+
const available = output?.embedding_available ?? output?.ollama_available;
|
|
1885
|
+
const model = text(output?.model)?.trim();
|
|
1886
|
+
const protocol = text(output?.protocol)?.trim();
|
|
1887
|
+
const totalInsights = number(output?.total_insights);
|
|
1888
|
+
const embedded = number(output?.embedded);
|
|
1889
|
+
const coverage = text(output?.coverage)?.trim();
|
|
1890
|
+
if (typeof available !== "boolean" || model === void 0 || model === "" || model.length > 200 || /[\u0000-\u001f\u007f]/u.test(model) || !Number.isInteger(totalInsights) || totalInsights < 0 || !Number.isInteger(embedded) || embedded < 0 || embedded > totalInsights || coverage === void 0 || !/^(?:100|\d{1,2})%$/u.test(coverage)) throw new Error("mnemon embed --status returned an invalid response");
|
|
1891
|
+
return {
|
|
1892
|
+
available,
|
|
1893
|
+
model,
|
|
1894
|
+
totalInsights,
|
|
1895
|
+
embedded,
|
|
1896
|
+
coverage,
|
|
1897
|
+
...protocol !== void 0 && protocol.length <= 32 && !/[\u0000-\u001f\u007f]/u.test(protocol) ? { protocol } : {}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
/** How long one fill of a space's vectors may run, and the longest step of it that holds the storage lock (#392). */
|
|
1901
|
+
const EMBED_FILL_BUDGET_MS = 6e4;
|
|
1902
|
+
const EMBED_FILL_STEP_MS = 1e4;
|
|
1903
|
+
/** A whole percent, as Mnemon writes it, that reads 100% only when every memory has its vector. */
|
|
1904
|
+
function coveragePercent(embedded, total) {
|
|
1905
|
+
return total === 0 ? "0%" : `${Math.floor(embedded * 100 / total)}%`;
|
|
1906
|
+
}
|
|
1907
|
+
/** Mnemon's reason for a failed call, without the usage it prints after its `Error:` line. */
|
|
1908
|
+
function cliReason(error) {
|
|
1909
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1910
|
+
return /\bError: ([^\n]+)/u.exec(detail)?.[1]?.trim() ?? detail.split("\n")[0].trim();
|
|
1911
|
+
}
|
|
1912
|
+
/** Whether a CLI call failed for this cause. */
|
|
1913
|
+
function cliFailure(error, reason) {
|
|
1914
|
+
return error instanceof MnemonCliError && error.reason === reason;
|
|
1915
|
+
}
|
|
1489
1916
|
var MemorySpacesService = class {
|
|
1490
1917
|
runner;
|
|
1491
1918
|
config;
|
|
@@ -1624,7 +2051,15 @@ var MemorySpacesService = class {
|
|
|
1624
2051
|
total: items.length,
|
|
1625
2052
|
activeCount: items.filter((body) => body.active && body.providerEnabled !== false).length,
|
|
1626
2053
|
directory: this.memorySpaces.directory,
|
|
1627
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2054
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2055
|
+
rediscovery: this.rediscoveryState()
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
rediscoveryState() {
|
|
2059
|
+
const entry = rediscoveries.get(this.memorySpaces.providerRegistryPath);
|
|
2060
|
+
return {
|
|
2061
|
+
running: entry?.running !== void 0,
|
|
2062
|
+
...entry?.last === void 0 ? {} : { last: entry.last }
|
|
1628
2063
|
};
|
|
1629
2064
|
}
|
|
1630
2065
|
/** Stable, secret-free checkpoint for the projected Memory Space authority. */
|
|
@@ -1702,23 +2137,137 @@ var MemorySpacesService = class {
|
|
|
1702
2137
|
...version === void 0 ? {} : { version }
|
|
1703
2138
|
};
|
|
1704
2139
|
}
|
|
1705
|
-
/**
|
|
2140
|
+
/**
|
|
2141
|
+
* Probe Mnemon's embedding runtime, and count the vectors of every Mnemon Native space:
|
|
2142
|
+
* `mnemon embed --status` counts only one Store, so a full count for it could read as the
|
|
2143
|
+
* health of every space while another one lacked vectors (#392).
|
|
2144
|
+
*/
|
|
1706
2145
|
async embeddingStatus(signal) {
|
|
1707
|
-
const
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2146
|
+
const native = this.memorySpaces.list().filter((body) => this.isNativeSpace(body) && existsSync(body.dbPath));
|
|
2147
|
+
let runtime;
|
|
2148
|
+
const spaces = [];
|
|
2149
|
+
for (const body of native) {
|
|
2150
|
+
const space = {
|
|
2151
|
+
memoryBodyId: body.id,
|
|
2152
|
+
name: body.name,
|
|
2153
|
+
active: body.active
|
|
2154
|
+
};
|
|
2155
|
+
try {
|
|
2156
|
+
const counts = embeddingCounts(await this.runner.runJson(["embed", "--status"], {
|
|
2157
|
+
store: body.id,
|
|
2158
|
+
database: body.dbPath,
|
|
2159
|
+
...signal === void 0 ? {} : { signal }
|
|
2160
|
+
}));
|
|
2161
|
+
runtime ??= counts;
|
|
2162
|
+
spaces.push({
|
|
2163
|
+
...space,
|
|
2164
|
+
totalInsights: counts.totalInsights,
|
|
2165
|
+
embedded: counts.embedded,
|
|
2166
|
+
coverage: coveragePercent(counts.embedded, counts.totalInsights)
|
|
2167
|
+
});
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
if (signal?.aborted === true) throw error;
|
|
2170
|
+
if (cliFailure(error, "no-database")) continue;
|
|
2171
|
+
spaces.push({
|
|
2172
|
+
...space,
|
|
2173
|
+
totalInsights: 0,
|
|
2174
|
+
embedded: 0,
|
|
2175
|
+
coverage: "0%",
|
|
2176
|
+
error: cliReason(error)
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
runtime ??= await this.embeddingRuntime(signal);
|
|
2181
|
+
const counted = spaces.filter((space) => space.error === void 0);
|
|
2182
|
+
const totalInsights = counted.reduce((sum, space) => sum + space.totalInsights, 0);
|
|
2183
|
+
const embedded = counted.reduce((sum, space) => sum + space.embedded, 0);
|
|
1715
2184
|
return {
|
|
1716
|
-
|
|
1717
|
-
model,
|
|
2185
|
+
...runtime,
|
|
1718
2186
|
totalInsights,
|
|
1719
2187
|
embedded,
|
|
1720
|
-
coverage,
|
|
1721
|
-
|
|
2188
|
+
coverage: coveragePercent(embedded, totalInsights),
|
|
2189
|
+
spaces
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
/** Mnemon's embedding runtime, asked in a scratch data directory: no space could be counted, and Mnemon creates the Store it opens. */
|
|
2193
|
+
async embeddingRuntime(signal) {
|
|
2194
|
+
const directory = mkdtempSync(join(tmpdir(), "dsh-mnemon-embedding-"));
|
|
2195
|
+
try {
|
|
2196
|
+
return embeddingCounts(await this.runner.runJson([
|
|
2197
|
+
"--data-dir",
|
|
2198
|
+
directory,
|
|
2199
|
+
"embed",
|
|
2200
|
+
"--status"
|
|
2201
|
+
], {
|
|
2202
|
+
globalFlags: false,
|
|
2203
|
+
...signal === void 0 ? {} : { signal }
|
|
2204
|
+
}));
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
throw new Error(cliReason(error), { cause: error });
|
|
2207
|
+
} finally {
|
|
2208
|
+
rmSync(directory, {
|
|
2209
|
+
recursive: true,
|
|
2210
|
+
force: true
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Fill in the vectors a Mnemon Native space lacks, through the Host's own data directory and
|
|
2216
|
+
* embedding settings, which a terminal `mnemon embed --all` may not share (#392). Mnemon fills at
|
|
2217
|
+
* most 100 a call and keeps the vectors it wrote when a call is stopped, so the fill goes in steps
|
|
2218
|
+
* no longer than one CLI call, letting other memory work take the storage lock between them,
|
|
2219
|
+
* until none is missing, a step writes none, or a minute is up.
|
|
2220
|
+
*/
|
|
2221
|
+
async embedMissing(memoryBodyId, signal) {
|
|
2222
|
+
this.assertWritable();
|
|
2223
|
+
const body = this.memorySpaces.get(validateMemorySpaceId(memoryBodyId));
|
|
2224
|
+
if (!this.isNativeSpace(body)) throw new Error(`${body.provider.label} keeps no Mnemon vectors to fill in`);
|
|
2225
|
+
const options = {
|
|
2226
|
+
store: body.id,
|
|
2227
|
+
database: body.dbPath,
|
|
2228
|
+
...signal === void 0 ? {} : { signal }
|
|
2229
|
+
};
|
|
2230
|
+
const failed = (error) => new Error(cliFailure(error, "no-database") ? `Memory Space ${body.name} has no database` : cliReason(error), { cause: error });
|
|
2231
|
+
const count = async () => {
|
|
2232
|
+
try {
|
|
2233
|
+
return embeddingCounts(await this.runner.runJson(["embed", "--status"], options));
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
throw failed(error);
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
const step = Math.min(this.config.timeoutMs, EMBED_FILL_STEP_MS);
|
|
2239
|
+
const deadline = Date.now() + EMBED_FILL_BUDGET_MS;
|
|
2240
|
+
const first = await count();
|
|
2241
|
+
let last = first;
|
|
2242
|
+
let stopped;
|
|
2243
|
+
while (last.embedded < last.totalInsights) {
|
|
2244
|
+
if (Date.now() + step > deadline) {
|
|
2245
|
+
stopped = "time";
|
|
2246
|
+
break;
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
await this.runner.runText(["embed", "--all"], {
|
|
2250
|
+
...options,
|
|
2251
|
+
timeoutMs: step
|
|
2252
|
+
});
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
if (!cliFailure(error, "timeout")) throw failed(error);
|
|
2255
|
+
}
|
|
2256
|
+
const next = await count();
|
|
2257
|
+
const progressed = next.embedded > last.embedded;
|
|
2258
|
+
last = next;
|
|
2259
|
+
if (!progressed) {
|
|
2260
|
+
stopped = "refused";
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
const remaining = last.totalInsights - last.embedded;
|
|
2265
|
+
return {
|
|
2266
|
+
memoryBodyId: body.id,
|
|
2267
|
+
filled: Math.max(0, last.embedded - first.embedded),
|
|
2268
|
+
remaining,
|
|
2269
|
+
model: last.model,
|
|
2270
|
+
...remaining > 0 && stopped !== void 0 ? { stopped } : {}
|
|
1722
2271
|
};
|
|
1723
2272
|
}
|
|
1724
2273
|
async status(signal) {
|
|
@@ -2509,6 +3058,89 @@ var MemorySpacesService = class {
|
|
|
2509
3058
|
const discovered = await provider.discover(connection, signal);
|
|
2510
3059
|
return this.memorySpaces.syncProviderService(providerId, connection, discovered);
|
|
2511
3060
|
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Ask every enabled Provider service again for its namespaces, as saving its settings does, and
|
|
3063
|
+
* mount the ones this directory has never held, inactive (#373). Nothing mounted changes and a
|
|
3064
|
+
* namespace a person disconnected stays disconnected. Providers are asked side by side, each
|
|
3065
|
+
* for at most 30 seconds; one that cannot answer keeps its spaces and says why. A pass already
|
|
3066
|
+
* running for this directory, at start-up or from the page, is joined rather than run twice; a
|
|
3067
|
+
* request that joined one its starter then stopped asks again, unless it was stopped itself.
|
|
3068
|
+
*/
|
|
3069
|
+
async rediscoverProviders(trigger, signal, providerTimeoutMs = MEMORY_SPACES_REDISCOVERY_PROVIDER_TIMEOUT_MS) {
|
|
3070
|
+
this.assertWritable();
|
|
3071
|
+
const key = this.memorySpaces.providerRegistryPath;
|
|
3072
|
+
const entry = rediscoveries.get(key) ?? {};
|
|
3073
|
+
rediscoveries.set(key, entry);
|
|
3074
|
+
const joined = entry.running;
|
|
3075
|
+
if (joined !== void 0) try {
|
|
3076
|
+
return await (signal === void 0 ? joined : untilAborted(joined, signal));
|
|
3077
|
+
} catch {
|
|
3078
|
+
signal?.throwIfAborted();
|
|
3079
|
+
const next = entry.running;
|
|
3080
|
+
if (next !== void 0 && next !== joined) return await (signal === void 0 ? next : untilAborted(next, signal));
|
|
3081
|
+
}
|
|
3082
|
+
const running = this.runRediscovery(trigger, signal, providerTimeoutMs);
|
|
3083
|
+
entry.running = running;
|
|
3084
|
+
try {
|
|
3085
|
+
const outcome = await running;
|
|
3086
|
+
entry.last = outcome;
|
|
3087
|
+
return outcome;
|
|
3088
|
+
} finally {
|
|
3089
|
+
if (entry.running === running) delete entry.running;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
async runRediscovery(trigger, signal, providerTimeoutMs) {
|
|
3093
|
+
const services = this.memorySpaces.providerServices().items.filter((item) => item.enabled && !this.isNativeProvider(item.providerId));
|
|
3094
|
+
const results = await Promise.all(services.map(async (item) => {
|
|
3095
|
+
const provider = this.providers.get(item.providerId);
|
|
3096
|
+
if (provider?.discover === void 0) return void 0;
|
|
3097
|
+
const label = this.providerCatalog.descriptor(item.providerId).label;
|
|
3098
|
+
const timeout = AbortSignal.timeout(providerTimeoutMs);
|
|
3099
|
+
const bounded = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
3100
|
+
try {
|
|
3101
|
+
const connection = this.memorySpaces.resolveProviderService(item.providerId, {});
|
|
3102
|
+
const discovered = await untilAborted(provider.discover(connection, bounded), bounded);
|
|
3103
|
+
signal?.throwIfAborted();
|
|
3104
|
+
const mounted = this.memorySpaces.mountDiscovered(item.providerId, connection, discovered);
|
|
3105
|
+
return {
|
|
3106
|
+
providerId: item.providerId,
|
|
3107
|
+
label,
|
|
3108
|
+
found: discovered.length,
|
|
3109
|
+
added: mounted.added.map((body) => ({
|
|
3110
|
+
id: body.id,
|
|
3111
|
+
name: body.name
|
|
3112
|
+
})),
|
|
3113
|
+
disconnected: mounted.disconnected
|
|
3114
|
+
};
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
signal?.throwIfAborted();
|
|
3117
|
+
const failure = {
|
|
3118
|
+
providerId: item.providerId,
|
|
3119
|
+
label,
|
|
3120
|
+
found: 0,
|
|
3121
|
+
added: [],
|
|
3122
|
+
disconnected: 0
|
|
3123
|
+
};
|
|
3124
|
+
if (timeout.aborted) {
|
|
3125
|
+
const limit = providerTimeoutMs < 1e3 ? `${providerTimeoutMs} ms` : `${Math.round(providerTimeoutMs / 1e3)} seconds`;
|
|
3126
|
+
return {
|
|
3127
|
+
...failure,
|
|
3128
|
+
timedOut: true,
|
|
3129
|
+
error: `did not answer within ${limit}`
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
return {
|
|
3133
|
+
...failure,
|
|
3134
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
}));
|
|
3138
|
+
return {
|
|
3139
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3140
|
+
trigger,
|
|
3141
|
+
results: results.filter((result) => result !== void 0)
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
2512
3144
|
updateSpace(id, request) {
|
|
2513
3145
|
this.assertWritable();
|
|
2514
3146
|
return this.memorySpaces.update(id, request);
|
|
@@ -2707,129 +3339,6 @@ var MemorySpacesService = class {
|
|
|
2707
3339
|
}
|
|
2708
3340
|
};
|
|
2709
3341
|
//#endregion
|
|
2710
|
-
//#region src/runner.ts
|
|
2711
|
-
var MnemonCliError = class extends Error {
|
|
2712
|
-
constructor(message) {
|
|
2713
|
-
super(message);
|
|
2714
|
-
this.name = "MnemonCliError";
|
|
2715
|
-
}
|
|
2716
|
-
};
|
|
2717
|
-
const EMBEDDING_ENVIRONMENT_KEYS = /* @__PURE__ */ new Set([
|
|
2718
|
-
"MNEMON_EMBED_ENDPOINT",
|
|
2719
|
-
"MNEMON_EMBED_MODEL",
|
|
2720
|
-
"MNEMON_EMBED_API_KEY",
|
|
2721
|
-
"MNEMON_EMBED_PROTOCOL"
|
|
2722
|
-
]);
|
|
2723
|
-
/**
|
|
2724
|
-
* Explain a CLI call that produced no result. Only a failed launch means the
|
|
2725
|
-
* executable is missing or unusable, so only that case carries the install
|
|
2726
|
-
* hint; a timeout, a cancellation or an oversized output names its own cause.
|
|
2727
|
-
*/
|
|
2728
|
-
function processFailureMessage(error, args) {
|
|
2729
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2730
|
-
const reason = error instanceof ProcessError ? error.reason : "launch";
|
|
2731
|
-
const command = args.find((arg) => !arg.startsWith("-"));
|
|
2732
|
-
if (reason === "output-limit") return `mnemon ${command ?? "command"} stopped: ${detail.replace(/^mnemon /u, "")}`;
|
|
2733
|
-
if (reason === "timeout" && command !== void 0) return detail.replace(/^mnemon /u, `mnemon ${command} `);
|
|
2734
|
-
if (reason !== "launch") return detail;
|
|
2735
|
-
return `${detail}. ${process.platform === "win32" ? "Install the official Mnemon Windows release, ensure mnemon.exe is on PATH or under %LOCALAPPDATA%\\Programs\\mnemon, or set MNEMON_CLI_PATH or mnemon.cliPath to its absolute path." : "Install Mnemon and ensure \"mnemon\" is on PATH, or set MNEMON_CLI_PATH or mnemon.cliPath."}`;
|
|
2736
|
-
}
|
|
2737
|
-
/** Preserve the Host environment while making saved embedding overrides authoritative. */
|
|
2738
|
-
function processEnvironment(config) {
|
|
2739
|
-
if (!config.embedding.enabled) return void 0;
|
|
2740
|
-
return {
|
|
2741
|
-
...Object.fromEntries(Object.entries(process.env).filter(([key]) => !EMBEDDING_ENVIRONMENT_KEYS.has(key.toUpperCase()))),
|
|
2742
|
-
MNEMON_EMBED_ENDPOINT: config.embedding.endpoint,
|
|
2743
|
-
MNEMON_EMBED_MODEL: config.embedding.model,
|
|
2744
|
-
MNEMON_EMBED_API_KEY: config.embedding.apiKey,
|
|
2745
|
-
...config.embedding.protocol === "auto" ? {} : { MNEMON_EMBED_PROTOCOL: config.embedding.protocol }
|
|
2746
|
-
};
|
|
2747
|
-
}
|
|
2748
|
-
function createRunner(config, processRunner = runProcess) {
|
|
2749
|
-
const currentCommand = () => findMnemonCommand(config) ?? config.cliPath ?? "mnemon";
|
|
2750
|
-
const globalArgs = (store) => {
|
|
2751
|
-
const args = ["--data-dir", effectiveDataDir()];
|
|
2752
|
-
if (store !== void 0) args.push("--store", store);
|
|
2753
|
-
else if (config.store !== void 0) args.push("--store", config.store);
|
|
2754
|
-
return args;
|
|
2755
|
-
};
|
|
2756
|
-
const effectiveDataDir = () => config.dataDir;
|
|
2757
|
-
const persistedStore = () => {
|
|
2758
|
-
const active = join(effectiveDataDir(), "active");
|
|
2759
|
-
if (existsSync(active)) try {
|
|
2760
|
-
const value = readFileSync(active, "utf8").trim();
|
|
2761
|
-
if (/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) return value;
|
|
2762
|
-
} catch {}
|
|
2763
|
-
return "default";
|
|
2764
|
-
};
|
|
2765
|
-
const launch = async (args, options = {}) => {
|
|
2766
|
-
if (options.signal?.aborted === true) throw new MnemonCliError(`mnemon command aborted: ${String(options.signal.reason ?? "cancelled")}`);
|
|
2767
|
-
const argv = options.globalFlags === false ? [...args] : [...globalArgs(options.store), ...args];
|
|
2768
|
-
const environment = processEnvironment(config);
|
|
2769
|
-
const processOptions = {
|
|
2770
|
-
timeoutMs: config.timeoutMs,
|
|
2771
|
-
...environment === void 0 ? {} : { env: environment },
|
|
2772
|
-
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
2773
|
-
...options.maxOutputBytes === void 0 ? {} : { maxOutputBytes: options.maxOutputBytes }
|
|
2774
|
-
};
|
|
2775
|
-
let result;
|
|
2776
|
-
try {
|
|
2777
|
-
const invocation = resolveMnemonInvocation(currentCommand(), argv, environment);
|
|
2778
|
-
result = await processRunner(invocation.command, invocation.args, {
|
|
2779
|
-
...processOptions,
|
|
2780
|
-
...invocation.env === void 0 ? {} : { env: invocation.env }
|
|
2781
|
-
});
|
|
2782
|
-
} catch (error) {
|
|
2783
|
-
throw new MnemonCliError(processFailureMessage(error, args));
|
|
2784
|
-
}
|
|
2785
|
-
if (result.exitCode !== 0) {
|
|
2786
|
-
const detail = result.stderr.trim() || result.stdout.trim() || "no output";
|
|
2787
|
-
throw new MnemonCliError(`mnemon ${args.join(" ")} exited ${String(result.exitCode)}: ${detail}`);
|
|
2788
|
-
}
|
|
2789
|
-
return result.stdout;
|
|
2790
|
-
};
|
|
2791
|
-
const execute = (args, options = {}) => {
|
|
2792
|
-
return withMemoryStorageLock(effectiveDataDir(), () => launch(args, options));
|
|
2793
|
-
};
|
|
2794
|
-
return {
|
|
2795
|
-
get command() {
|
|
2796
|
-
return currentCommand();
|
|
2797
|
-
},
|
|
2798
|
-
get commandFound() {
|
|
2799
|
-
const found = findMnemonCommand(config);
|
|
2800
|
-
return found !== void 0 && isMnemonExecutable(found);
|
|
2801
|
-
},
|
|
2802
|
-
async runJson(args, options) {
|
|
2803
|
-
const stdout = await execute(args, options);
|
|
2804
|
-
try {
|
|
2805
|
-
return JSON.parse(stdout);
|
|
2806
|
-
} catch {
|
|
2807
|
-
throw new MnemonCliError(`mnemon ${args.join(" ")} returned invalid JSON`);
|
|
2808
|
-
}
|
|
2809
|
-
},
|
|
2810
|
-
runText: execute,
|
|
2811
|
-
runTextBatch(commands) {
|
|
2812
|
-
return withMemoryStorageLock(effectiveDataDir(), async () => {
|
|
2813
|
-
const outputs = [];
|
|
2814
|
-
for (const command of commands) outputs.push(await launch(command.args, command.options));
|
|
2815
|
-
return outputs;
|
|
2816
|
-
});
|
|
2817
|
-
},
|
|
2818
|
-
effectiveDataDir() {
|
|
2819
|
-
return effectiveDataDir();
|
|
2820
|
-
},
|
|
2821
|
-
persistedStore() {
|
|
2822
|
-
return persistedStore();
|
|
2823
|
-
},
|
|
2824
|
-
effectiveStore() {
|
|
2825
|
-
if (config.store !== void 0) return config.store;
|
|
2826
|
-
const fromEnvironment = process.env.MNEMON_STORE?.trim();
|
|
2827
|
-
if (fromEnvironment !== void 0 && fromEnvironment !== "") return fromEnvironment;
|
|
2828
|
-
return persistedStore();
|
|
2829
|
-
}
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
//#endregion
|
|
2833
3342
|
//#region src/config.ts
|
|
2834
3343
|
const MemorySpacesConfig = z.object({
|
|
2835
3344
|
dataDir: z.string(),
|
|
@@ -3073,6 +3582,28 @@ function placementDecision(value) {
|
|
|
3073
3582
|
...memoryInputText(input.subagentProvider, "placement decision subagentProvider", 500, false) === void 0 ? {} : { subagentProvider: memoryInputText(input.subagentProvider, "placement decision subagentProvider", 500, false) }
|
|
3074
3583
|
};
|
|
3075
3584
|
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Why this turn's View cannot forget from a Memory Space it does not read (#390). The View is
|
|
3587
|
+
* pinned for the turn, so switching a space on reaches a forget from the next turn only, and
|
|
3588
|
+
* whether a space is on stays the user's choice.
|
|
3589
|
+
*/
|
|
3590
|
+
function outsideView(service, memoryBodyId) {
|
|
3591
|
+
const catalog = service.spaceDirectory();
|
|
3592
|
+
const body = catalog.items.find((candidate) => candidate.id === memoryBodyId);
|
|
3593
|
+
if (body === void 0) return `Memory Space ${JSON.stringify(memoryBodyId)} does not exist`;
|
|
3594
|
+
const space = `Memory Space ${JSON.stringify(memoryBodyId)} (${body.name})`;
|
|
3595
|
+
if (!body.active) return `${space} is switched off, so this turn's View does not read it and cannot forget from it. Whether it is on is the user's choice, on the Memory Spaces page; once it is on, a forget there works from the next turn`;
|
|
3596
|
+
if (body.providerEnabled === false) return `${space} uses ${body.provider.label}, which is turned off on the dsh-mnemon page under Plugins`;
|
|
3597
|
+
const provider = catalog.providers.find((candidate) => candidate.id === body.provider.id);
|
|
3598
|
+
if (provider?.serviceConfigured === false) return `${space} uses ${body.provider.label}, which is not available here${(provider.typeId ?? provider.id) === "mnemon-native" ? ": the Mnemon CLI is not installed" : ""}`;
|
|
3599
|
+
return `${space} was switched on after this turn began, so this turn's View does not read it yet; a forget there works from the next turn`;
|
|
3600
|
+
}
|
|
3601
|
+
/** The switched-off spaces a forget cannot reach this turn, named only when there are some (#390). */
|
|
3602
|
+
function switchedOffSpaces(service) {
|
|
3603
|
+
const off = service.spaceDirectory().items.filter((body) => !body.active);
|
|
3604
|
+
if (off.length === 0) return "";
|
|
3605
|
+
return `. Switched-off spaces (${off.slice(0, 5).map((body) => JSON.stringify(body.name)).join(", ")}${off.length > 5 ? ` and ${off.length - 5} more` : ""}) are outside this View; one the user switches on is read from the next turn`;
|
|
3606
|
+
}
|
|
3076
3607
|
function managementResult(service, value) {
|
|
3077
3608
|
return {
|
|
3078
3609
|
revision: service.memoryRevision(),
|
|
@@ -3129,6 +3660,7 @@ async function manageMemorySpaces(service, request) {
|
|
|
3129
3660
|
}
|
|
3130
3661
|
if (!request.confirmed) throw new Error("Memory Spaces management mutation requires explicit confirmation");
|
|
3131
3662
|
switch (request.operation) {
|
|
3663
|
+
case "embedding-fill": return managementResult(service, await service.embedMissing(memoryInputText(input.memoryBodyId, "memoryBodyId", 300), request.signal));
|
|
3132
3664
|
case "provider-service-update": {
|
|
3133
3665
|
const providerId = memoryInputText(input.providerId, "providerId", 128);
|
|
3134
3666
|
await service.updateProviderService(providerId, scalarRecord(input.settings, "settings") ?? {}, memoryInputStringArray(input.clearSecrets, "clearSecrets", 100) ?? [], input.enabled === void 0 ? true : input.enabled === true, request.signal);
|
|
@@ -3180,6 +3712,7 @@ async function manageMemorySpaces(service, request) {
|
|
|
3180
3712
|
case "reload":
|
|
3181
3713
|
service.memorySpaces.reload();
|
|
3182
3714
|
return managementResult(service, { reloaded: true });
|
|
3715
|
+
case "provider-rediscover": return managementResult(service, await service.rediscoverProviders(input.trigger === "startup" ? "startup" : "manual", request.signal));
|
|
3183
3716
|
case "body-delete": return managementResult(service, await service.deleteSpace(memoryInputText(input.memoryBodyId, "memoryBodyId", 300), request.signal));
|
|
3184
3717
|
default: throw new Error(`unsupported Memory Spaces management mutation operation: ${request.operation}`);
|
|
3185
3718
|
}
|
|
@@ -3678,8 +4211,10 @@ function createMemorySpacesSource(providerSnapshot, config = {}) {
|
|
|
3678
4211
|
result = await service.link(sourceId, targetId, edge, weight, memoryInputText(input.reason, "reason", 1e3, false), request.signal, bodyId);
|
|
3679
4212
|
} else if (request.offer.sourceActionId === "forget") {
|
|
3680
4213
|
const id = memoryInputText(input.id, "id", 2e3);
|
|
4214
|
+
const named = memoryInputText(input.memoryBodyId, "memoryBodyId", 300, false);
|
|
4215
|
+
if (named !== void 0 && !allowedBodies.includes(named)) throw new Error(outsideView(service, named));
|
|
3681
4216
|
bodyId = await admittedOwner(id);
|
|
3682
|
-
if (bodyId === void 0 || !allowedBodies.includes(bodyId)) throw new Error(
|
|
4217
|
+
if (bodyId === void 0 || !allowedBodies.includes(bodyId)) throw new Error(`forget requires evidence already admitted by this View, or an id found in one Memory Space it can read; name the space (memoryBodyId) when several hold it${switchedOffSpaces(service)}`);
|
|
3683
4218
|
result = await service.forget(id, request.signal, bodyId);
|
|
3684
4219
|
} else throw new Error(`unsupported Memory Spaces action: ${request.offer.sourceActionId}`);
|
|
3685
4220
|
return createMemoryMutationReceipt(request.view.id, request.offer.id, context.sourceInstanceKey, service.memoryRevision(), {
|