@runeya/runeya 2.0.121 → 2.0.123

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.
@@ -0,0 +1,117 @@
1
+ import {
2
+ machineRoot
3
+ } from "./chunk-ZFELQG4B.js";
4
+
5
+ // ../server/src/services/disk-freshness.ts
6
+ import { stat } from "fs/promises";
7
+ var seen = /* @__PURE__ */ new Map();
8
+ async function rememberFile(path) {
9
+ try {
10
+ const s = await stat(path);
11
+ seen.set(path, { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino });
12
+ } catch {
13
+ seen.delete(path);
14
+ }
15
+ }
16
+ async function hasChangedSinceRead(path) {
17
+ const known = seen.get(path);
18
+ if (!known) return true;
19
+ try {
20
+ const s = await stat(path);
21
+ return s.mtimeMs !== known.mtimeMs || s.size !== known.size || s.ino !== known.ino;
22
+ } catch {
23
+ return true;
24
+ }
25
+ }
26
+ function wasRead(path) {
27
+ return seen.has(path);
28
+ }
29
+ async function fingerprintFiles(paths) {
30
+ const stamps = await Promise.all(paths.map(async (path) => {
31
+ try {
32
+ const s = await stat(path);
33
+ return `${path}:${s.mtimeMs}:${s.size}:${s.ino}`;
34
+ } catch {
35
+ return `${path}:-`;
36
+ }
37
+ }));
38
+ return stamps.sort().join("|");
39
+ }
40
+
41
+ // ../server/src/services/run-store-utils.ts
42
+ import { randomUUID } from "crypto";
43
+ import { chmod, mkdir, readdir, rename, rm, stat as stat2, writeFile, readFile } from "fs/promises";
44
+ import { dirname, join, resolve, sep } from "path";
45
+ var SAFE_PATH_SEGMENT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
46
+ function resolveConversationRunDir(rootDirName, conversationId) {
47
+ if (!SAFE_PATH_SEGMENT_RE.test(conversationId)) {
48
+ throw new Error("Invalid conversationId");
49
+ }
50
+ const base = resolve(machineRoot(), rootDirName);
51
+ const dir = resolve(base, conversationId);
52
+ if (!dir.startsWith(base + sep)) {
53
+ throw new Error("Invalid conversationId");
54
+ }
55
+ return dir;
56
+ }
57
+ async function atomicWriteSecure(filePath, content) {
58
+ await mkdir(dirname(filePath), { recursive: true });
59
+ const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
60
+ try {
61
+ await writeFile(tmpPath, content, typeof content === "string" ? "utf-8" : void 0);
62
+ await rename(tmpPath, filePath);
63
+ } catch (err) {
64
+ await rm(tmpPath, { force: true }).catch(() => {
65
+ });
66
+ throw err;
67
+ }
68
+ await chmod(filePath, 384);
69
+ }
70
+ var OWN_TEMP_RE = /\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/;
71
+ var STALE_TEMP_MS = 36e5;
72
+ async function sweepStaleTempFiles(dirs) {
73
+ let removed = 0;
74
+ const limit = Date.now() - STALE_TEMP_MS;
75
+ for (const dir of dirs) {
76
+ let names;
77
+ try {
78
+ names = await readdir(dir);
79
+ } catch {
80
+ continue;
81
+ }
82
+ for (const name of names.filter((n) => OWN_TEMP_RE.test(n))) {
83
+ const path = join(dir, name);
84
+ try {
85
+ if ((await stat2(path)).mtimeMs > limit) continue;
86
+ await rm(path, { force: true });
87
+ removed += 1;
88
+ } catch {
89
+ }
90
+ }
91
+ }
92
+ return removed;
93
+ }
94
+ async function atomicWriteJsonSecure(filePath, data, pretty = false) {
95
+ await atomicWriteSecure(filePath, JSON.stringify(data, null, pretty ? 2 : 0));
96
+ }
97
+ async function readJsonFileSafe(filePath) {
98
+ try {
99
+ const raw = await readFile(filePath, "utf-8");
100
+ return JSON.parse(raw);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ export {
107
+ rememberFile,
108
+ hasChangedSinceRead,
109
+ wasRead,
110
+ fingerprintFiles,
111
+ resolveConversationRunDir,
112
+ atomicWriteSecure,
113
+ sweepStaleTempFiles,
114
+ atomicWriteJsonSecure,
115
+ readJsonFileSafe
116
+ };
117
+ //# sourceMappingURL=chunk-HK5QNU6P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../server/src/services/disk-freshness.ts","../../server/src/services/run-store-utils.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\n\n/**\n * Le fichier a-t-il changé depuis qu'on l'a lu ?\n *\n * Les stores gardent de côté ce qu'ils n'ont pas su déchiffrer, pour le\n * réécrire tel quel plutôt que de l'effacer. Cette copie est la bonne réponse\n * tant que personne d'autre ne touche au fichier — et la mauvaise dès que\n * quelqu'un le répare par-dessous : la copie, elle, ne se périme jamais, et la\n * sauvegarde suivante réinstalle l'état d'avant la réparation.\n *\n * C'est arrivé deux fois de suite sur le même coffre : une conversion de clé\n * menée sur le disque, défaite quelques minutes plus tard par un store qui\n * tenait encore l'ancien chiffré en mémoire.\n *\n * D'où ce repère : l'empreinte du fichier au moment de la lecture. Si elle a\n * bougé, la copie mémoire ne fait plus foi et doit être abandonnée.\n */\n\ninterface Fingerprint {\n mtimeMs: number;\n size: number;\n /**\n * L'inode du chemin, qui change à chaque `rename` par-dessus.\n *\n * La date et la taille laissent passer le motif d'écriture de tout le dépôt :\n * les stores réécrivent le fichier entier, donc une modification rend souvent\n * le même nombre d'octets, et deux instances qui réagissent au même événement\n * écrivent dans la même milliseconde. Le fichier temporaire renommé\n * par-dessus, lui, installe toujours un autre inode.\n *\n * Là où le système n'en expose pas (il rend alors 0), le repère retombe sur\n * la date et la taille — ce qu'il valait avant.\n */\n ino: number;\n}\n\nconst seen = new Map<string, Fingerprint>();\n\n/** Retenir l'état d'un fichier au moment où on vient de le lire. */\nexport async function rememberFile(path: string): Promise<void> {\n try {\n const s = await stat(path);\n seen.set(path, { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino });\n } catch {\n // Pas encore de fichier : rien à retenir, et rien à réémettre non plus.\n seen.delete(path);\n }\n}\n\n/**\n * Vrai si le fichier a changé — ou si l'on ne sait pas.\n *\n * L'ignorance compte comme un changement : réécrire une copie dont on ne peut\n * plus prouver qu'elle correspond au disque est précisément le geste à éviter.\n */\nexport async function hasChangedSinceRead(path: string): Promise<boolean> {\n const known = seen.get(path);\n if (!known) return true;\n try {\n const s = await stat(path);\n return s.mtimeMs !== known.mtimeMs || s.size !== known.size || s.ino !== known.ino;\n } catch {\n return true;\n }\n}\n\n/**\n * A-t-on lu ce fichier ?\n *\n * `hasChangedSinceRead` tient l'ignorance pour un changement : c'est juste\n * quand il s'agit de décider si une copie mémoire fait encore foi. Ça ne l'est\n * pas pour décider d'écrire — un fichier jamais lu est le cas d'un coffre neuf,\n * et le traiter comme « modifié sous nos pieds » revenait à ne jamais écrire la\n * première fois. Les appelants qui protègent une écriture demandent donc les\n * deux : connu, et inchangé.\n */\nexport function wasRead(path: string): boolean {\n return seen.has(path);\n}\n\n/** Oublier un fichier — après un rechargement complet, par exemple. */\nexport function forgetFile(path: string): void {\n seen.delete(path);\n}\n\n/**\n * L'empreinte d'un ensemble de fichiers : lesquels, et dans quel état.\n *\n * Le repère ci-dessus répond pour un fichier qu'on a lu soi-même ; celui-ci\n * répond pour un store dont le contenu vient de plusieurs fichiers à la fois —\n * un par coffre — et dont la question est « le disque a-t-il bougé depuis mon\n * dernier chargement ? ». Comparer deux empreintes coûte quelques `stat` ;\n * recharger coûte autant de lectures, d'analyses et de déchiffrements.\n *\n * L'absence d'un fichier en fait partie : un coffre disparu change l'empreinte\n * autant qu'un coffre modifié.\n */\nexport async function fingerprintFiles(paths: string[]): Promise<string> {\n const stamps = await Promise.all(paths.map(async (path) => {\n try {\n const s = await stat(path);\n // L'inode en fait partie pour la raison décrite sur `Fingerprint` : un\n // fichier renommé par-dessus la cible peut n'avoir bougé ni en taille ni\n // en date, et reste pourtant un autre contenu.\n return `${path}:${s.mtimeMs}:${s.size}:${s.ino}`;\n } catch {\n return `${path}:-`;\n }\n }));\n return stamps.sort().join('|');\n}\n","import { randomUUID } from 'node:crypto';\nimport { chmod, mkdir, readdir, rename, rm, stat, writeFile, readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { machineRoot } from './vault-resolver.js';\n\nconst SAFE_PATH_SEGMENT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;\n\nexport function resolveConversationRunDir(rootDirName: string, conversationId: string): string {\n if (!SAFE_PATH_SEGMENT_RE.test(conversationId)) {\n throw new Error('Invalid conversationId');\n }\n // Machine-wide, like the conversations these runs belong to: using the launch\n // directory would put a `.runeya` in a repository just for asking the AI a\n // question about a cloud project.\n const base = resolve(machineRoot(), rootDirName);\n const dir = resolve(base, conversationId);\n if (!dir.startsWith(base + sep)) {\n throw new Error('Invalid conversationId');\n }\n return dir;\n}\n\nexport async function atomicWriteSecure(filePath: string, content: string | Uint8Array): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n // Un nom par écriture : deux Runeya d'un même poste écrivent les mêmes\n // fichiers, et un `.tmp` commun se fait renommer sous le nez de l'autre.\n const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmpPath, content, typeof content === 'string' ? 'utf-8' : undefined);\n await rename(tmpPath, filePath);\n } catch (err) {\n // Un nom unique ne se réécrit jamais : laissé là, chaque échec en ajouterait un.\n await rm(tmpPath, { force: true }).catch(() => {});\n throw err;\n }\n await chmod(filePath, 0o600);\n}\n\n/** Le nom qu'`atomicWriteSecure` donne à ses fichiers temporaires : `<fichier>.<pid>.<uuid>.tmp`. */\nconst OWN_TEMP_RE = /\\.\\d+\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.tmp$/;\nconst STALE_TEMP_MS = 3600_000;\n\n/**\n * Retirer les fichiers temporaires qu'un arrêt brutal a laissés.\n *\n * Un nom par écriture ne se réécrit jamais : chaque arrêt en pleine écriture en\n * laissait un pour toujours. Seuls les nôtres sont concernés, et seulement\n * passé une heure — un plus récent peut appartenir à une écriture en cours,\n * celle d'une autre Runeya du même poste.\n */\nexport async function sweepStaleTempFiles(dirs: string[]): Promise<number> {\n let removed = 0;\n const limit = Date.now() - STALE_TEMP_MS;\n for (const dir of dirs) {\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n continue;\n }\n for (const name of names.filter((n) => OWN_TEMP_RE.test(n))) {\n const path = join(dir, name);\n try {\n if ((await stat(path)).mtimeMs > limit) continue;\n await rm(path, { force: true });\n removed += 1;\n } catch {\n // Disparu entre-temps : rien à retirer.\n }\n }\n }\n return removed;\n}\n\nexport async function atomicWriteJsonSecure(filePath: string, data: unknown, pretty = false): Promise<void> {\n await atomicWriteSecure(filePath, JSON.stringify(data, null, pretty ? 2 : 0));\n}\n\nexport async function readJsonFileSafe<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await readFile(filePath, 'utf-8');\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":["stat","stat"]}
@@ -1,9 +1,10 @@
1
1
  import {
2
+ atomicWriteSecure,
2
3
  fingerprintFiles,
3
4
  hasChangedSinceRead,
4
5
  rememberFile,
5
6
  wasRead
6
- } from "./chunk-YZU5FNXR.js";
7
+ } from "./chunk-HK5QNU6P.js";
7
8
  import {
8
9
  AGENT_FETCH_TIMEOUT,
9
10
  AGENT_HEALTH_POLL_INTERVAL,
@@ -390,7 +391,7 @@ var agentStore = new AgentStore();
390
391
 
391
392
  // ../server/src/services/service-store.ts
392
393
  import { randomUUID as randomUUID3 } from "crypto";
393
- import { readFile as readFile3, writeFile as writeFile3, rename as rename3, mkdir as mkdir3, chmod as chmod3, rm as rm2 } from "fs/promises";
394
+ import { readFile as readFile3, rm as rm2 } from "fs/promises";
394
395
  import { join as join3 } from "path";
395
396
 
396
397
  // ../server/src/services/project-key.ts
@@ -501,9 +502,49 @@ function isVaultConflicted(root) {
501
502
  return conflicted.has(root);
502
503
  }
503
504
 
505
+ // ../server/src/services/store-disk-coordinator.ts
506
+ import { AsyncLocalStorage } from "async_hooks";
507
+ var insideReload = new AsyncLocalStorage();
508
+ var StoreDiskCoordinator = class {
509
+ reloading = null;
510
+ saves = 0;
511
+ queue = Promise.resolve();
512
+ /**
513
+ * `read` rend de quoi substituer le nouvel état, ou `null` s'il n'y a rien à
514
+ * relire. La substitution doit être synchrone : c'est ce qui la rend atomique.
515
+ */
516
+ load(read) {
517
+ const inside = insideReload.getStore();
518
+ if (inside && (inside.has(this) || this.reloading)) return Promise.resolve();
519
+ this.reloading ??= this.reload(read).finally(() => {
520
+ this.reloading = null;
521
+ });
522
+ return this.reloading;
523
+ }
524
+ async reload(read) {
525
+ for (; ; ) {
526
+ await this.queue;
527
+ const savesBefore = this.saves;
528
+ const outer = insideReload.getStore() ?? /* @__PURE__ */ new Set();
529
+ const commit = await insideReload.run(/* @__PURE__ */ new Set([...outer, this]), read);
530
+ if (!commit) return;
531
+ if (this.saves !== savesBefore) continue;
532
+ commit();
533
+ return;
534
+ }
535
+ }
536
+ save(write) {
537
+ this.saves += 1;
538
+ const run = this.queue.then(write);
539
+ this.queue = run.catch(() => {
540
+ });
541
+ return run;
542
+ }
543
+ };
544
+
504
545
  // ../server/src/services/environment-store.ts
505
546
  import { randomUUID as randomUUID2 } from "crypto";
506
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2, chmod as chmod2, rm } from "fs/promises";
547
+ import { readFile as readFile2, rm } from "fs/promises";
507
548
  import { join as join2 } from "path";
508
549
 
509
550
  // ../server/src/services/crypto-helpers.ts
@@ -647,6 +688,7 @@ var EnvironmentStore = class {
647
688
  * refusée l'est pour toujours et la modification de l'utilisateur se perd.
648
689
  */
649
690
  fingerprint = "";
691
+ disk = new StoreDiskCoordinator();
650
692
  listeners = /* @__PURE__ */ new Set();
651
693
  /** Notified after every persisted mutation, for the cloud sync bridge. */
652
694
  onChange(fn) {
@@ -810,7 +852,7 @@ var EnvironmentStore = class {
810
852
  }
811
853
  return result;
812
854
  }
813
- decryptEnvironment(environment, vault) {
855
+ decryptEnvironment(environment, vault, unreadable = this.unreadableSecrets) {
814
856
  let decryptedVars = environment.variables;
815
857
  if (environment.variables && Object.keys(environment.variables).length > 0) {
816
858
  const failed = /* @__PURE__ */ new Set();
@@ -820,8 +862,8 @@ var EnvironmentStore = class {
820
862
  (variableId) => failed.add(variableId),
821
863
  vault ?? this.vaultOf(environment)
822
864
  );
823
- if (failed.size > 0) this.unreadableSecrets.set(environment.id, failed);
824
- else this.unreadableSecrets.delete(environment.id);
865
+ if (failed.size > 0) unreadable.set(environment.id, failed);
866
+ else unreadable.delete(environment.id);
825
867
  }
826
868
  return { ...environment, variables: decryptedVars };
827
869
  }
@@ -837,39 +879,47 @@ var EnvironmentStore = class {
837
879
  * store reads environments while absorbing a project, which would deadlock).
838
880
  */
839
881
  async load() {
840
- const fingerprint = await this.diskFingerprint();
841
- if (this.loaded && fingerprint === this.fingerprint) return;
842
- this.loaded = true;
843
- this.fingerprint = fingerprint;
844
- await ensureProjectsLoaded();
845
- if (!getEncryptionKey()) {
846
- this.loaded = false;
847
- return;
848
- }
849
- const roots = await allVaultRoots();
850
- this.environments.clear();
851
- this.diskEncryptedSlots.clear();
852
- this.roots.clear();
853
- this.unreadableSecrets.clear();
854
- for (const root of roots) {
882
+ await this.disk.load(async () => {
883
+ const fingerprint = await this.diskFingerprint();
884
+ if (this.loaded && fingerprint === this.fingerprint) return null;
885
+ await ensureProjectsLoaded();
886
+ if (!getEncryptionKey()) return null;
887
+ const next = await this.readVaults();
888
+ return () => {
889
+ this.environments = next.environments;
890
+ this.diskEncryptedSlots = next.diskEncryptedSlots;
891
+ this.roots = next.roots;
892
+ this.unreadableSecrets = next.unreadableSecrets;
893
+ this.fingerprint = fingerprint;
894
+ this.loaded = true;
895
+ };
896
+ });
897
+ }
898
+ /** Lire tous les coffres dans des tables neuves, que `load` substitue d'un coup. */
899
+ async readVaults() {
900
+ const environments = /* @__PURE__ */ new Map();
901
+ const diskEncryptedSlots = /* @__PURE__ */ new Map();
902
+ const roots = /* @__PURE__ */ new Map();
903
+ const unreadableSecrets = /* @__PURE__ */ new Map();
904
+ for (const root of await allVaultRoots()) {
855
905
  try {
856
906
  const raw = await readFile2(this.getFilePath(root), "utf-8");
857
907
  await rememberFile(this.getFilePath(root));
858
908
  const data = JSON.parse(raw);
859
- const clash = data.find((e) => this.environments.has(e.id) && this.roots.get(e.id) !== root);
909
+ const clash = data.find((e) => environments.has(e.id) && roots.get(e.id) !== root);
860
910
  if (clash) {
861
911
  markVaultConflict(root, `environnement ${clash.id}`);
862
912
  continue;
863
913
  }
864
914
  for (const e of data) {
865
- this.diskEncryptedSlots.set(e.id, this.extractEncryptedCache(e));
866
- this.roots.set(e.id, root);
867
- const decrypted = this.decryptEnvironment(e, vaultIdOfRoot(root));
868
- this.environments.set(e.id, decrypted);
915
+ diskEncryptedSlots.set(e.id, this.extractEncryptedCache(e));
916
+ roots.set(e.id, root);
917
+ environments.set(e.id, this.decryptEnvironment(e, vaultIdOfRoot(root), unreadableSecrets));
869
918
  }
870
919
  } catch {
871
920
  }
872
921
  }
922
+ return { environments, diskEncryptedSlots, roots, unreadableSecrets };
873
923
  }
874
924
  /**
875
925
  * Write each vault that holds — or held — an environment.
@@ -891,7 +941,10 @@ var EnvironmentStore = class {
891
941
  if (wasRead(path) && await hasChangedSinceRead(path)) this.staleRoots.add(root);
892
942
  }
893
943
  }
894
- async save() {
944
+ save() {
945
+ return this.disk.save(() => this.writeVaults());
946
+ }
947
+ async writeVaults() {
895
948
  await this.markStaleRoots();
896
949
  const byRoot = /* @__PURE__ */ new Map();
897
950
  for (const root of this.roots.values()) {
@@ -921,11 +974,7 @@ var EnvironmentStore = class {
921
974
  });
922
975
  continue;
923
976
  }
924
- await mkdir2(root, { recursive: true });
925
- const tmpPath = filePath + ".tmp";
926
- await writeFile2(tmpPath, JSON.stringify(environments, null, 2), "utf-8");
927
- await rename2(tmpPath, filePath);
928
- await chmod2(filePath, 384);
977
+ await atomicWriteSecure(filePath, JSON.stringify(environments, null, 2));
929
978
  await rememberFile(filePath);
930
979
  }
931
980
  this.fingerprint = await this.diskFingerprint();
@@ -1273,6 +1322,7 @@ var ServiceStore = class {
1273
1322
  * pour toutes ne verrait jamais les services créés par l'autre.
1274
1323
  */
1275
1324
  fingerprint = "";
1325
+ disk = new StoreDiskCoordinator();
1276
1326
  listeners = /* @__PURE__ */ new Set();
1277
1327
  /**
1278
1328
  * Last on-disk (encrypted) form per service id, so `prepareForDisk` can reuse
@@ -1344,20 +1394,30 @@ var ServiceStore = class {
1344
1394
  */
1345
1395
  async load() {
1346
1396
  if (!getEncryptionKey()) return;
1347
- const fingerprint = await this.diskFingerprint();
1348
- if (this.loaded && fingerprint === this.fingerprint) return;
1349
- this.loaded = true;
1350
- this.fingerprint = fingerprint;
1351
- await ensureProjectsLoaded();
1352
- const roots = await allVaultRoots();
1353
- this.services.clear();
1354
- this.diskForm.clear();
1355
- this.roots.clear();
1356
- for (const root of roots) {
1397
+ await this.disk.load(async () => {
1398
+ const fingerprint = await this.diskFingerprint();
1399
+ if (this.loaded && fingerprint === this.fingerprint) return null;
1400
+ await ensureProjectsLoaded();
1401
+ const next = await this.readVaults();
1402
+ return () => {
1403
+ this.services = next.services;
1404
+ this.diskForm = next.diskForm;
1405
+ this.roots = next.roots;
1406
+ this.fingerprint = fingerprint;
1407
+ this.loaded = true;
1408
+ if (this.services.size > 0) this.notify();
1409
+ };
1410
+ });
1411
+ }
1412
+ async readVaults() {
1413
+ const services = /* @__PURE__ */ new Map();
1414
+ const diskForm = /* @__PURE__ */ new Map();
1415
+ const roots = /* @__PURE__ */ new Map();
1416
+ for (const root of await allVaultRoots()) {
1357
1417
  try {
1358
1418
  const raw = await readFile3(this.getFilePath(root), "utf-8");
1359
1419
  const data = JSON.parse(raw);
1360
- const clash = data.find((s) => this.services.has(s.id) && this.roots.get(s.id) !== root);
1420
+ const clash = data.find((s) => services.has(s.id) && roots.get(s.id) !== root);
1361
1421
  if (clash) {
1362
1422
  markVaultConflict(root, `service ${clash.id}`);
1363
1423
  continue;
@@ -1368,17 +1428,17 @@ var ServiceStore = class {
1368
1428
  if (cmd.shell === void 0) cmd.shell = true;
1369
1429
  }
1370
1430
  }
1371
- this.diskForm.set(s.id, s);
1372
- this.services.set(s.id, {
1431
+ diskForm.set(s.id, s);
1432
+ services.set(s.id, {
1373
1433
  ...s,
1374
1434
  logSources: decryptLogSources(s.logSources ?? [], vaultIdOfRoot(root))
1375
1435
  });
1376
- this.roots.set(s.id, root);
1436
+ roots.set(s.id, root);
1377
1437
  }
1378
1438
  } catch {
1379
1439
  }
1380
1440
  }
1381
- if (this.services.size > 0) this.notify();
1441
+ return { services, diskForm, roots };
1382
1442
  }
1383
1443
  /**
1384
1444
  * Write each vault that holds — or held — a service.
@@ -1387,7 +1447,10 @@ var ServiceStore = class {
1387
1447
  * another vault must not survive in the one it left, and a `[]` left behind
1388
1448
  * would keep (or recreate) a `.runeya` in a directory that holds nothing.
1389
1449
  */
1390
- async save() {
1450
+ save() {
1451
+ return this.disk.save(() => this.writeVaults());
1452
+ }
1453
+ async writeVaults() {
1391
1454
  const byRoot = /* @__PURE__ */ new Map();
1392
1455
  for (const root of this.roots.values()) {
1393
1456
  if (!isVaultConflicted(root)) byRoot.set(root, []);
@@ -1410,11 +1473,7 @@ var ServiceStore = class {
1410
1473
  });
1411
1474
  continue;
1412
1475
  }
1413
- await mkdir3(root, { recursive: true });
1414
- const tmpPath = filePath + ".tmp";
1415
- await writeFile3(tmpPath, JSON.stringify(services, null, 2), "utf-8");
1416
- await rename3(tmpPath, filePath);
1417
- await chmod3(filePath, 384);
1476
+ await atomicWriteSecure(filePath, JSON.stringify(services, null, 2));
1418
1477
  }
1419
1478
  this.fingerprint = await this.diskFingerprint();
1420
1479
  }
@@ -1627,7 +1686,7 @@ import { dirname as dirname2, join as join5 } from "path";
1627
1686
  import { fileURLToPath } from "url";
1628
1687
 
1629
1688
  // ../server/src/services/instance-registry.ts
1630
- import { mkdir as mkdir4, readdir, readFile as readFile4, writeFile as writeFile4, rm as rm3 } from "fs/promises";
1689
+ import { mkdir as mkdir2, readdir, readFile as readFile4, writeFile as writeFile2, rm as rm3 } from "fs/promises";
1631
1690
  import { readFileSync } from "fs";
1632
1691
  import dotenv from "dotenv";
1633
1692
  import { spawn } from "child_process";
@@ -1724,9 +1783,9 @@ function selfRecord() {
1724
1783
  }
1725
1784
  async function registerInstance() {
1726
1785
  try {
1727
- await mkdir4(registryDir(), { recursive: true });
1786
+ await mkdir2(registryDir(), { recursive: true });
1728
1787
  published = selfRecord();
1729
- await writeFile4(fileFor(process.pid), JSON.stringify(published, null, 2));
1788
+ await writeFile2(fileFor(process.pid), JSON.stringify(published, null, 2));
1730
1789
  } catch (err) {
1731
1790
  console.log("[instances] Could not register this instance:", err.message);
1732
1791
  }
@@ -1739,7 +1798,7 @@ async function setInstanceAgentPid(agentPid) {
1739
1798
  if (!published) return;
1740
1799
  published = { ...published, agentPid: agentPid ?? void 0 };
1741
1800
  try {
1742
- await writeFile4(fileFor(process.pid), JSON.stringify(published, null, 2));
1801
+ await writeFile2(fileFor(process.pid), JSON.stringify(published, null, 2));
1743
1802
  } catch {
1744
1803
  }
1745
1804
  }
@@ -3011,6 +3070,7 @@ export {
3011
3070
  decryptVariableSlots,
3012
3071
  extractEncryptedVariableSlots,
3013
3072
  isEncryptedValue,
3073
+ StoreDiskCoordinator,
3014
3074
  projectKey,
3015
3075
  AmbiguousProjectError,
3016
3076
  registerProjectLoader,
@@ -3035,4 +3095,4 @@ export {
3035
3095
  traefikManager,
3036
3096
  agentManager
3037
3097
  };
3038
- //# sourceMappingURL=chunk-CNTL6NVU.js.map
3098
+ //# sourceMappingURL=chunk-MSLFR2EN.js.map