agent-relay 4.0.31 → 4.0.32

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.
Files changed (43) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +210 -75
  6. package/node_modules/@agent-relay/cloud/package.json +2 -2
  7. package/node_modules/@agent-relay/config/package.json +1 -1
  8. package/node_modules/@agent-relay/hooks/package.json +4 -4
  9. package/node_modules/@agent-relay/sdk/README.md +3 -0
  10. package/node_modules/@agent-relay/sdk/dist/relay.d.ts +16 -0
  11. package/node_modules/@agent-relay/sdk/dist/relay.d.ts.map +1 -1
  12. package/node_modules/@agent-relay/sdk/dist/relay.js +83 -0
  13. package/node_modules/@agent-relay/sdk/dist/relay.js.map +1 -1
  14. package/node_modules/@agent-relay/sdk/package.json +2 -2
  15. package/node_modules/@agent-relay/telemetry/package.json +1 -1
  16. package/node_modules/@agent-relay/trajectory/package.json +2 -2
  17. package/node_modules/@agent-relay/user-directory/package.json +2 -2
  18. package/node_modules/@agent-relay/utils/package.json +2 -2
  19. package/node_modules/agent-trajectories/dist/{chunk-2XT3DOJC.js → chunk-27AQPWHK.js} +136 -72
  20. package/node_modules/agent-trajectories/dist/chunk-27AQPWHK.js.map +1 -0
  21. package/node_modules/agent-trajectories/dist/cli/index.js +135 -71
  22. package/node_modules/agent-trajectories/dist/cli/index.js.map +1 -1
  23. package/node_modules/agent-trajectories/dist/{index-thTh5iI8.d.ts → index-C7XhwsoN.d.ts} +24 -0
  24. package/node_modules/agent-trajectories/dist/index.d.ts +2 -2
  25. package/node_modules/agent-trajectories/dist/index.js +1 -1
  26. package/node_modules/agent-trajectories/dist/sdk/index.d.ts +1 -1
  27. package/node_modules/agent-trajectories/dist/sdk/index.js +1 -1
  28. package/node_modules/agent-trajectories/package.json +1 -1
  29. package/package.json +9 -9
  30. package/packages/cloud/package.json +2 -2
  31. package/packages/config/package.json +1 -1
  32. package/packages/hooks/package.json +4 -4
  33. package/packages/sdk/README.md +3 -0
  34. package/packages/sdk/dist/relay.d.ts +16 -0
  35. package/packages/sdk/dist/relay.d.ts.map +1 -1
  36. package/packages/sdk/dist/relay.js +83 -0
  37. package/packages/sdk/dist/relay.js.map +1 -1
  38. package/packages/sdk/package.json +2 -2
  39. package/packages/telemetry/package.json +1 -1
  40. package/packages/trajectory/package.json +2 -2
  41. package/packages/user-directory/package.json +2 -2
  42. package/packages/utils/package.json +2 -2
  43. package/node_modules/agent-trajectories/dist/chunk-2XT3DOJC.js.map +0 -1
@@ -404,8 +404,16 @@ function abandonTrajectory(trajectory, reason) {
404
404
  }
405
405
 
406
406
  // src/storage/file.ts
407
+ import { randomUUID } from "crypto";
407
408
  import { existsSync } from "fs";
408
- import { mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
409
+ import {
410
+ mkdir,
411
+ readFile,
412
+ readdir,
413
+ rename,
414
+ unlink,
415
+ writeFile
416
+ } from "fs/promises";
409
417
  import { join } from "path";
410
418
 
411
419
  // src/export/markdown.ts
@@ -586,6 +594,16 @@ function getSearchPaths() {
586
594
  }
587
595
  return [join(process.cwd(), ".trajectories")];
588
596
  }
597
+ var indexLocks = /* @__PURE__ */ new Map();
598
+ function withIndexLock(path2, task) {
599
+ const prev = indexLocks.get(path2) ?? Promise.resolve();
600
+ const next = prev.then(task, task);
601
+ indexLocks.set(
602
+ path2,
603
+ next.catch(() => void 0)
604
+ );
605
+ return next;
606
+ }
589
607
  var FileStorage = class {
590
608
  baseDir;
591
609
  trajectoriesDir;
@@ -612,10 +630,10 @@ var FileStorage = class {
612
630
  await mkdir(this.activeDir, { recursive: true });
613
631
  await mkdir(this.completedDir, { recursive: true });
614
632
  if (!existsSync(this.indexPath)) {
615
- await this.saveIndex({
616
- version: 1,
617
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
618
- trajectories: {}
633
+ await withIndexLock(this.indexPath, async () => {
634
+ if (!existsSync(this.indexPath)) {
635
+ await this.saveIndex(this.emptyIndex());
636
+ }
619
637
  });
620
638
  }
621
639
  await this.reconcileIndex();
@@ -643,49 +661,51 @@ var FileStorage = class {
643
661
  skippedSchemaViolation: 0,
644
662
  skippedIoError: 0
645
663
  };
646
- const index = await this.loadIndex();
647
- const before = Object.keys(index.trajectories).length;
648
- const discovered = [];
649
- try {
650
- const activeFiles = await readdir(this.activeDir);
651
- for (const file of activeFiles) {
652
- if (!file.endsWith(".json")) continue;
653
- discovered.push(join(this.activeDir, file));
664
+ await withIndexLock(this.indexPath, async () => {
665
+ const index = await this.loadIndex();
666
+ const before = Object.keys(index.trajectories).length;
667
+ const discovered = [];
668
+ try {
669
+ const activeFiles = await readdir(this.activeDir);
670
+ for (const file of activeFiles) {
671
+ if (!file.endsWith(".json")) continue;
672
+ discovered.push(join(this.activeDir, file));
673
+ }
674
+ } catch (error) {
675
+ if (error.code !== "ENOENT") throw error;
654
676
  }
655
- } catch (error) {
656
- if (error.code !== "ENOENT") throw error;
657
- }
658
- await this.walkJsonFilesInto(this.completedDir, discovered);
659
- for (const filePath of discovered) {
660
- summary.scanned += 1;
661
- const result = await this.readTrajectoryFile(filePath);
662
- if (!result.ok) {
663
- if (result.reason === "malformed_json") {
664
- summary.skippedMalformedJson += 1;
665
- } else if (result.reason === "schema_violation") {
666
- summary.skippedSchemaViolation += 1;
667
- } else {
668
- summary.skippedIoError += 1;
677
+ await this.walkJsonFilesInto(this.completedDir, discovered);
678
+ for (const filePath of discovered) {
679
+ summary.scanned += 1;
680
+ const result = await this.readTrajectoryFile(filePath);
681
+ if (!result.ok) {
682
+ if (result.reason === "malformed_json") {
683
+ summary.skippedMalformedJson += 1;
684
+ } else if (result.reason === "schema_violation") {
685
+ summary.skippedSchemaViolation += 1;
686
+ } else {
687
+ summary.skippedIoError += 1;
688
+ }
689
+ continue;
669
690
  }
670
- continue;
691
+ const trajectory = result.trajectory;
692
+ if (index.trajectories[trajectory.id]) {
693
+ summary.alreadyIndexed += 1;
694
+ continue;
695
+ }
696
+ index.trajectories[trajectory.id] = {
697
+ title: trajectory.task.title,
698
+ status: trajectory.status,
699
+ startedAt: trajectory.startedAt,
700
+ completedAt: trajectory.completedAt,
701
+ path: filePath
702
+ };
703
+ summary.added += 1;
671
704
  }
672
- const trajectory = result.trajectory;
673
- if (index.trajectories[trajectory.id]) {
674
- summary.alreadyIndexed += 1;
675
- continue;
705
+ if (Object.keys(index.trajectories).length !== before) {
706
+ await this.saveIndex(index);
676
707
  }
677
- index.trajectories[trajectory.id] = {
678
- title: trajectory.task.title,
679
- status: trajectory.status,
680
- startedAt: trajectory.startedAt,
681
- completedAt: trajectory.completedAt,
682
- path: filePath
683
- };
684
- summary.added += 1;
685
- }
686
- if (Object.keys(index.trajectories).length !== before) {
687
- await this.saveIndex(index);
688
- }
708
+ });
689
709
  const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
690
710
  if (summary.added > 0 || hadSkips) {
691
711
  const parts = [`reconciled ${summary.added}/${summary.scanned}`];
@@ -888,17 +908,19 @@ var FileStorage = class {
888
908
  if (existsSync(activePath)) {
889
909
  await unlink(activePath);
890
910
  }
891
- const index = await this.loadIndex();
892
- const entry = index.trajectories[id];
893
- if (entry?.path && existsSync(entry.path)) {
894
- await unlink(entry.path);
895
- const mdPath = entry.path.replace(".json", ".md");
896
- if (existsSync(mdPath)) {
897
- await unlink(mdPath);
911
+ await withIndexLock(this.indexPath, async () => {
912
+ const index = await this.loadIndex();
913
+ const entry = index.trajectories[id];
914
+ if (entry?.path && existsSync(entry.path)) {
915
+ await unlink(entry.path);
916
+ const mdPath = entry.path.replace(".json", ".md");
917
+ if (existsSync(mdPath)) {
918
+ await unlink(mdPath);
919
+ }
898
920
  }
899
- }
900
- delete index.trajectories[id];
901
- await this.saveIndex(index);
921
+ delete index.trajectories[id];
922
+ await this.saveIndex(index);
923
+ });
902
924
  }
903
925
  /**
904
926
  * Search trajectories by text
@@ -976,10 +998,23 @@ var FileStorage = class {
976
998
  const result = await this.readTrajectoryFile(path2);
977
999
  return result.ok ? result.trajectory : null;
978
1000
  }
1001
+ /**
1002
+ * Read and parse the on-disk index.
1003
+ *
1004
+ * Tolerances (belt-and-braces against the read/write race):
1005
+ * - ENOENT: first-run, return an empty index silently.
1006
+ * - Empty file: a concurrent writer truncated index.json in "w" mode
1007
+ * right before we read. Return an empty index silently — this is
1008
+ * not a real corruption, just an interleaving the mutex + atomic
1009
+ * rename should already prevent. Logging here would be noise.
1010
+ * - Non-empty but malformed JSON: genuinely corrupted on disk (hand
1011
+ * edit, disk error, etc). Log it and return an empty index so the
1012
+ * caller can recover, but keep the log so the problem is visible.
1013
+ */
979
1014
  async loadIndex() {
1015
+ let content;
980
1016
  try {
981
- const content = await readFile(this.indexPath, "utf-8");
982
- return JSON.parse(content);
1017
+ content = await readFile(this.indexPath, "utf-8");
983
1018
  } catch (error) {
984
1019
  if (error.code !== "ENOENT") {
985
1020
  console.error(
@@ -987,27 +1022,56 @@ var FileStorage = class {
987
1022
  error
988
1023
  );
989
1024
  }
990
- return {
991
- version: 1,
992
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
993
- trajectories: {}
994
- };
1025
+ return this.emptyIndex();
1026
+ }
1027
+ if (content.length === 0) {
1028
+ return this.emptyIndex();
1029
+ }
1030
+ try {
1031
+ return JSON.parse(content);
1032
+ } catch (error) {
1033
+ console.error(
1034
+ "Error loading trajectory index, using empty index:",
1035
+ error
1036
+ );
1037
+ return this.emptyIndex();
995
1038
  }
996
1039
  }
1040
+ emptyIndex() {
1041
+ return {
1042
+ version: 1,
1043
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
1044
+ trajectories: {}
1045
+ };
1046
+ }
1047
+ /**
1048
+ * Atomic write: stage into a process-unique temp path in the same directory
1049
+ * and then rename over the live file. `rename` is atomic on POSIX, so
1050
+ * concurrent readers in any process either see the old complete file or
1051
+ * the new complete file — never a half-written / zero-byte state.
1052
+ *
1053
+ * Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
1054
+ * read-modify-write cycle stays serialized; the unique temp name also keeps
1055
+ * parallel writers in other processes from colliding on a shared tmp path.
1056
+ */
997
1057
  async saveIndex(index) {
998
1058
  index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
999
- await writeFile(this.indexPath, JSON.stringify(index, null, 2), "utf-8");
1059
+ const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
1060
+ await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
1061
+ await rename(tmpPath, this.indexPath);
1000
1062
  }
1001
1063
  async updateIndex(trajectory, filePath) {
1002
- const index = await this.loadIndex();
1003
- index.trajectories[trajectory.id] = {
1004
- title: trajectory.task.title,
1005
- status: trajectory.status,
1006
- startedAt: trajectory.startedAt,
1007
- completedAt: trajectory.completedAt,
1008
- path: filePath
1009
- };
1010
- await this.saveIndex(index);
1064
+ await withIndexLock(this.indexPath, async () => {
1065
+ const index = await this.loadIndex();
1066
+ index.trajectories[trajectory.id] = {
1067
+ title: trajectory.task.title,
1068
+ status: trajectory.status,
1069
+ startedAt: trajectory.startedAt,
1070
+ completedAt: trajectory.completedAt,
1071
+ path: filePath
1072
+ };
1073
+ await this.saveIndex(index);
1074
+ });
1011
1075
  }
1012
1076
  };
1013
1077