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/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 { n as ProcessError, r as runProcess } from "./process-DIK9NnTg.js";
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 [this.registryPath, this.providerRegistryPath].map((path) => {
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
- /** Probe the effective Mnemon embedding runtime and its default Store coverage. */
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 output = record(await this.runner.runJson(["embed", "--status"], signal === void 0 ? {} : { signal }));
1708
- const available = output?.embedding_available ?? output?.ollama_available;
1709
- const model = text(output?.model)?.trim();
1710
- const protocol = text(output?.protocol)?.trim();
1711
- const totalInsights = number(output?.total_insights);
1712
- const embedded = number(output?.embedded);
1713
- const coverage = text(output?.coverage)?.trim();
1714
- 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");
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
- available,
1717
- model,
2185
+ ...runtime,
1718
2186
  totalInsights,
1719
2187
  embedded,
1720
- coverage,
1721
- ...protocol !== void 0 && protocol.length <= 32 && !/[\u0000-\u001f\u007f]/u.test(protocol) ? { protocol } : {}
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("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");
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(), {