agent-trajectories 0.5.7 → 0.5.9
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/README.md +2 -2
- package/dist/{chunk-27AQPWHK.js → chunk-JMH3Z5BB.js} +579 -262
- package/dist/chunk-JMH3Z5BB.js.map +1 -0
- package/dist/cli/index.js +707 -372
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-C7XhwsoN.d.ts → index-B4yIThRL.d.ts} +111 -39
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-27AQPWHK.js.map +0 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { dirname, resolve as resolvePath } from "path";
|
|
5
|
+
import { dirname as dirname2, resolve as resolvePath } from "path";
|
|
6
6
|
|
|
7
7
|
// src/core/id.ts
|
|
8
8
|
import { webcrypto } from "crypto";
|
|
@@ -666,39 +666,53 @@ function formatTime(isoString) {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
// src/storage/file.ts
|
|
669
|
-
import { randomUUID } from "crypto";
|
|
670
669
|
import { existsSync } from "fs";
|
|
671
670
|
import {
|
|
672
671
|
mkdir,
|
|
673
672
|
readFile,
|
|
674
673
|
readdir,
|
|
675
674
|
rename,
|
|
676
|
-
|
|
675
|
+
rm,
|
|
677
676
|
writeFile
|
|
678
677
|
} from "fs/promises";
|
|
679
|
-
import {
|
|
678
|
+
import {
|
|
679
|
+
basename,
|
|
680
|
+
dirname,
|
|
681
|
+
isAbsolute,
|
|
682
|
+
join,
|
|
683
|
+
relative,
|
|
684
|
+
resolve
|
|
685
|
+
} from "path";
|
|
686
|
+
var TRAJECTORY_FILE = "trajectory.json";
|
|
687
|
+
var SUMMARY_FILE = "summary.md";
|
|
688
|
+
var COMPACTION_FILE = "compaction.json";
|
|
689
|
+
var LEGACY_COMPACTION_SUFFIX = ".compaction.json";
|
|
680
690
|
function expandPath(path) {
|
|
681
691
|
if (path.startsWith("~")) {
|
|
682
692
|
return join(process.env.HOME ?? "", path.slice(1));
|
|
683
693
|
}
|
|
684
694
|
return path;
|
|
685
695
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
696
|
+
function describeReadFailure(reason, error) {
|
|
697
|
+
if (reason === "schema_violation" && error && typeof error === "object" && "issues" in error) {
|
|
698
|
+
const issues = error.issues ?? [];
|
|
699
|
+
if (issues.length > 0) {
|
|
700
|
+
const first = issues[0];
|
|
701
|
+
const where = first.path.length > 0 ? first.path.join(".") : "root";
|
|
702
|
+
const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
|
|
703
|
+
return `${where}: ${first.message}${extra}`;
|
|
704
|
+
}
|
|
705
|
+
return "schema validation failed";
|
|
706
|
+
}
|
|
707
|
+
if (error instanceof Error) return error.message;
|
|
708
|
+
return String(error);
|
|
695
709
|
}
|
|
696
710
|
var FileStorage = class {
|
|
697
711
|
baseDir;
|
|
698
712
|
trajectoriesDir;
|
|
699
713
|
activeDir;
|
|
700
714
|
completedDir;
|
|
701
|
-
|
|
715
|
+
lastReconcileSummary;
|
|
702
716
|
constructor(baseDir) {
|
|
703
717
|
this.baseDir = baseDir ?? process.cwd();
|
|
704
718
|
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
@@ -709,7 +723,6 @@ var FileStorage = class {
|
|
|
709
723
|
}
|
|
710
724
|
this.activeDir = join(this.trajectoriesDir, "active");
|
|
711
725
|
this.completedDir = join(this.trajectoriesDir, "completed");
|
|
712
|
-
this.indexPath = join(this.trajectoriesDir, "index.json");
|
|
713
726
|
}
|
|
714
727
|
/**
|
|
715
728
|
* Initialize storage directories
|
|
@@ -718,28 +731,22 @@ var FileStorage = class {
|
|
|
718
731
|
await mkdir(this.trajectoriesDir, { recursive: true });
|
|
719
732
|
await mkdir(this.activeDir, { recursive: true });
|
|
720
733
|
await mkdir(this.completedDir, { recursive: true });
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (!existsSync(this.indexPath)) {
|
|
724
|
-
await this.saveIndex(this.emptyIndex());
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
}
|
|
734
|
+
await this.migrateLegacyIndexCompactionMarkers();
|
|
735
|
+
await rm(join(this.trajectoriesDir, "index.json"), { force: true });
|
|
728
736
|
await this.reconcileIndex();
|
|
729
737
|
}
|
|
730
738
|
/**
|
|
731
|
-
* Scan active/ and completed/ recursively and
|
|
732
|
-
*
|
|
733
|
-
* only adds, never removes.
|
|
739
|
+
* Scan active/ and completed/ recursively and report trajectory files
|
|
740
|
+
* that can be loaded plus files that should be surfaced by doctor.
|
|
734
741
|
*
|
|
735
742
|
* Handles three on-disk layouts in completed/:
|
|
736
743
|
* - flat: completed/{id}.json (legacy workforce data)
|
|
737
|
-
* - monthly: completed/YYYY-MM/{id}.json (
|
|
744
|
+
* - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
|
|
745
|
+
* - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
|
|
738
746
|
* - nested: completed/.../{id}.json (defensive — any depth)
|
|
739
747
|
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* added.
|
|
748
|
+
* The method name is kept for callers such as `trail doctor`, but no
|
|
749
|
+
* shared index file is written.
|
|
743
750
|
*/
|
|
744
751
|
async reconcileIndex() {
|
|
745
752
|
const summary = {
|
|
@@ -748,55 +755,32 @@ var FileStorage = class {
|
|
|
748
755
|
alreadyIndexed: 0,
|
|
749
756
|
skippedMalformedJson: 0,
|
|
750
757
|
skippedSchemaViolation: 0,
|
|
751
|
-
skippedIoError: 0
|
|
758
|
+
skippedIoError: 0,
|
|
759
|
+
failures: []
|
|
752
760
|
};
|
|
753
|
-
await
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
if (error.code !== "ENOENT") throw error;
|
|
765
|
-
}
|
|
766
|
-
await this.walkJsonFilesInto(this.completedDir, discovered);
|
|
767
|
-
for (const filePath of discovered) {
|
|
768
|
-
summary.scanned += 1;
|
|
769
|
-
const result = await this.readTrajectoryFile(filePath);
|
|
770
|
-
if (!result.ok) {
|
|
771
|
-
if (result.reason === "malformed_json") {
|
|
772
|
-
summary.skippedMalformedJson += 1;
|
|
773
|
-
} else if (result.reason === "schema_violation") {
|
|
774
|
-
summary.skippedSchemaViolation += 1;
|
|
775
|
-
} else {
|
|
776
|
-
summary.skippedIoError += 1;
|
|
777
|
-
}
|
|
778
|
-
continue;
|
|
779
|
-
}
|
|
780
|
-
const trajectory2 = result.trajectory;
|
|
781
|
-
if (index.trajectories[trajectory2.id]) {
|
|
782
|
-
summary.alreadyIndexed += 1;
|
|
783
|
-
continue;
|
|
761
|
+
const discovered = await this.listTrajectoryFiles();
|
|
762
|
+
for (const filePath of discovered) {
|
|
763
|
+
summary.scanned += 1;
|
|
764
|
+
const result = await this.readTrajectoryFile(filePath);
|
|
765
|
+
if (!result.ok) {
|
|
766
|
+
if (result.reason === "malformed_json") {
|
|
767
|
+
summary.skippedMalformedJson += 1;
|
|
768
|
+
} else if (result.reason === "schema_violation") {
|
|
769
|
+
summary.skippedSchemaViolation += 1;
|
|
770
|
+
} else {
|
|
771
|
+
summary.skippedIoError += 1;
|
|
784
772
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
};
|
|
792
|
-
summary.added += 1;
|
|
793
|
-
}
|
|
794
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
795
|
-
await this.saveIndex(index);
|
|
773
|
+
summary.failures.push({
|
|
774
|
+
path: result.path,
|
|
775
|
+
reason: result.reason,
|
|
776
|
+
message: describeReadFailure(result.reason, result.error)
|
|
777
|
+
});
|
|
778
|
+
continue;
|
|
796
779
|
}
|
|
797
|
-
|
|
780
|
+
summary.added += 1;
|
|
781
|
+
}
|
|
798
782
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
799
|
-
if (
|
|
783
|
+
if (hadSkips) {
|
|
800
784
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
801
785
|
if (summary.skippedMalformedJson > 0) {
|
|
802
786
|
parts.push(`malformed: ${summary.skippedMalformedJson}`);
|
|
@@ -809,10 +793,76 @@ var FileStorage = class {
|
|
|
809
793
|
}
|
|
810
794
|
console.warn(`[trajectories] ${parts.join(", ")}`);
|
|
811
795
|
}
|
|
796
|
+
this.lastReconcileSummary = summary;
|
|
812
797
|
return summary;
|
|
813
798
|
}
|
|
814
799
|
/**
|
|
815
|
-
*
|
|
800
|
+
* Returns the most recent reconcile summary, if any. Lets the CLI
|
|
801
|
+
* inspect the failures collected during `initialize()` without having
|
|
802
|
+
* to re-walk the directory tree (and re-emit the warn line).
|
|
803
|
+
*/
|
|
804
|
+
getLastReconcileSummary() {
|
|
805
|
+
return this.lastReconcileSummary;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Move trajectory files that fail to load into `.trajectories/invalid/`
|
|
809
|
+
* so reconcile no longer scans them. Only quarantines parse and schema
|
|
810
|
+
* failures — transient io_error failures are left in place because the
|
|
811
|
+
* file may load fine on the next attempt.
|
|
812
|
+
*
|
|
813
|
+
* Returns the list of files that were moved (with their original paths
|
|
814
|
+
* and the destination directory) so the caller can report what changed.
|
|
815
|
+
*/
|
|
816
|
+
async quarantineInvalid() {
|
|
817
|
+
const summary = await this.reconcileIndex();
|
|
818
|
+
const targetDir = join(this.trajectoriesDir, "invalid");
|
|
819
|
+
const candidates = summary.failures.filter((f) => f.reason !== "io_error");
|
|
820
|
+
if (candidates.length === 0) {
|
|
821
|
+
return { moved: [], targetDir };
|
|
822
|
+
}
|
|
823
|
+
await mkdir(targetDir, { recursive: true });
|
|
824
|
+
const moved = [];
|
|
825
|
+
for (const failure of candidates) {
|
|
826
|
+
const dest = await this.resolveQuarantineDest(failure.path, targetDir);
|
|
827
|
+
try {
|
|
828
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
829
|
+
await rename(failure.path, dest);
|
|
830
|
+
moved.push(failure);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
console.warn(
|
|
833
|
+
`[trajectories] failed to quarantine ${failure.path}: ${error instanceof Error ? error.message : String(error)}`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return { moved, targetDir };
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Pick a destination path under `targetDir` for a quarantined file.
|
|
841
|
+
*
|
|
842
|
+
* Preserves the file's relative location under the trajectories root
|
|
843
|
+
* (e.g. `completed/2026-04/foo.json` → `invalid/completed/2026-04/foo.json`)
|
|
844
|
+
* so two invalid files that share a basename across `active/` and
|
|
845
|
+
* `completed/` don't collapse onto each other and silently overwrite.
|
|
846
|
+
*
|
|
847
|
+
* Falls back to a numeric-suffix scheme for paths that live outside
|
|
848
|
+
* the trajectories directory or that, after relative resolution, would
|
|
849
|
+
* still collide with something already quarantined.
|
|
850
|
+
*/
|
|
851
|
+
async resolveQuarantineDest(sourcePath, targetDir) {
|
|
852
|
+
const rel = relative(this.trajectoriesDir, sourcePath);
|
|
853
|
+
const safeRel = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : basename(sourcePath);
|
|
854
|
+
let dest = join(targetDir, safeRel);
|
|
855
|
+
if (!existsSync(dest)) return dest;
|
|
856
|
+
const ext = safeRel.endsWith(".json") ? ".json" : "";
|
|
857
|
+
const stem = ext ? safeRel.slice(0, -ext.length) : safeRel;
|
|
858
|
+
for (let i = 1; i < 1e3; i += 1) {
|
|
859
|
+
dest = join(targetDir, `${stem}.${i}${ext}`);
|
|
860
|
+
if (!existsSync(dest)) return dest;
|
|
861
|
+
}
|
|
862
|
+
return dest;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Recursively collect trajectory JSON file paths under `dir` into `out`.
|
|
816
866
|
* Silently treats a missing directory as empty.
|
|
817
867
|
*/
|
|
818
868
|
async walkJsonFilesInto(dir, out) {
|
|
@@ -827,7 +877,7 @@ var FileStorage = class {
|
|
|
827
877
|
const entryPath = join(dir, entry.name);
|
|
828
878
|
if (entry.isDirectory()) {
|
|
829
879
|
await this.walkJsonFilesInto(entryPath, out);
|
|
830
|
-
} else if (entry.isFile() && entry.name
|
|
880
|
+
} else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
|
|
831
881
|
out.push(entryPath);
|
|
832
882
|
}
|
|
833
883
|
}
|
|
@@ -851,56 +901,43 @@ var FileStorage = class {
|
|
|
851
901
|
}
|
|
852
902
|
const trajectory2 = validation.data;
|
|
853
903
|
const isCompleted = trajectory2.status === "completed" || trajectory2.status === "abandoned";
|
|
854
|
-
|
|
904
|
+
const existingPaths = await this.findTrajectoryFilePaths(trajectory2.id);
|
|
905
|
+
let trajectoryDir;
|
|
855
906
|
if (isCompleted) {
|
|
856
907
|
const date = new Date(trajectory2.completedAt ?? trajectory2.startedAt);
|
|
857
908
|
const monthDir = join(
|
|
858
909
|
this.completedDir,
|
|
859
910
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
860
911
|
);
|
|
861
|
-
|
|
862
|
-
filePath = join(monthDir, `${trajectory2.id}.json`);
|
|
863
|
-
const activePath = join(this.activeDir, `${trajectory2.id}.json`);
|
|
864
|
-
if (existsSync(activePath)) {
|
|
865
|
-
await unlink(activePath);
|
|
866
|
-
}
|
|
867
|
-
const mdPath = join(monthDir, `${trajectory2.id}.md`);
|
|
868
|
-
const markdown = exportToMarkdown(trajectory2);
|
|
869
|
-
await writeFile(mdPath, markdown, "utf-8");
|
|
912
|
+
trajectoryDir = join(monthDir, trajectory2.id);
|
|
870
913
|
} else {
|
|
871
|
-
|
|
914
|
+
trajectoryDir = join(this.activeDir, trajectory2.id);
|
|
915
|
+
}
|
|
916
|
+
const filePath = join(trajectoryDir, TRAJECTORY_FILE);
|
|
917
|
+
await this.removeTrajectoryFiles(existingPaths, filePath);
|
|
918
|
+
await mkdir(trajectoryDir, { recursive: true });
|
|
919
|
+
if (isCompleted) {
|
|
920
|
+
const markdown = exportToMarkdown(trajectory2);
|
|
921
|
+
await writeFile(join(trajectoryDir, SUMMARY_FILE), markdown, "utf-8");
|
|
872
922
|
}
|
|
873
923
|
await writeFile(filePath, JSON.stringify(trajectory2, null, 2), "utf-8");
|
|
874
|
-
await this.updateIndex(trajectory2, filePath);
|
|
875
924
|
}
|
|
876
925
|
/**
|
|
877
926
|
* Get a trajectory by ID
|
|
878
927
|
*/
|
|
879
928
|
async get(id) {
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const entry = index.trajectories[id];
|
|
886
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
887
|
-
return this.readTrajectoryOrNull(entry.path);
|
|
888
|
-
}
|
|
889
|
-
try {
|
|
890
|
-
const flatPath = join(this.completedDir, `${id}.json`);
|
|
891
|
-
if (existsSync(flatPath)) {
|
|
892
|
-
return this.readTrajectoryOrNull(flatPath);
|
|
929
|
+
for (const filePath of this.getActiveCandidatePaths(id)) {
|
|
930
|
+
if (!existsSync(filePath)) continue;
|
|
931
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
932
|
+
if (trajectory2?.id === id) {
|
|
933
|
+
return trajectory2;
|
|
893
934
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
} catch (error) {
|
|
902
|
-
if (error.code !== "ENOENT") {
|
|
903
|
-
console.error("Error searching completed trajectories:", error);
|
|
935
|
+
}
|
|
936
|
+
const paths = await this.findTrajectoryFilePaths(id);
|
|
937
|
+
for (const filePath of paths) {
|
|
938
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
939
|
+
if (trajectory2?.id === id) {
|
|
940
|
+
return trajectory2;
|
|
904
941
|
}
|
|
905
942
|
}
|
|
906
943
|
return null;
|
|
@@ -909,107 +946,61 @@ var FileStorage = class {
|
|
|
909
946
|
* Get the currently active trajectory
|
|
910
947
|
*/
|
|
911
948
|
async getActive() {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
915
|
-
if (jsonFiles.length === 0) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
let mostRecent = null;
|
|
919
|
-
let mostRecentTime = 0;
|
|
920
|
-
for (const file of jsonFiles) {
|
|
921
|
-
const trajectory2 = await this.readTrajectoryOrNull(
|
|
922
|
-
join(this.activeDir, file)
|
|
923
|
-
);
|
|
924
|
-
if (trajectory2) {
|
|
925
|
-
const startTime = new Date(trajectory2.startedAt).getTime();
|
|
926
|
-
if (startTime > mostRecentTime) {
|
|
927
|
-
mostRecentTime = startTime;
|
|
928
|
-
mostRecent = trajectory2;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
return mostRecent;
|
|
933
|
-
} catch (error) {
|
|
934
|
-
if (error.code === "ENOENT") {
|
|
935
|
-
return null;
|
|
936
|
-
}
|
|
937
|
-
console.error("Error reading active trajectories:", error);
|
|
949
|
+
const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
|
|
950
|
+
if (activeFiles.length === 0) {
|
|
938
951
|
return null;
|
|
939
952
|
}
|
|
953
|
+
let mostRecent = null;
|
|
954
|
+
let mostRecentTime = 0;
|
|
955
|
+
for (const filePath of activeFiles) {
|
|
956
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
957
|
+
if (trajectory2?.status !== "active") continue;
|
|
958
|
+
const startTime = new Date(trajectory2.startedAt).getTime();
|
|
959
|
+
if (startTime > mostRecentTime) {
|
|
960
|
+
mostRecentTime = startTime;
|
|
961
|
+
mostRecent = trajectory2;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return mostRecent;
|
|
940
965
|
}
|
|
941
966
|
/**
|
|
942
967
|
* List trajectories with optional filtering
|
|
943
968
|
*/
|
|
944
969
|
async list(query) {
|
|
945
|
-
|
|
946
|
-
let entries = Object.entries(index.trajectories);
|
|
970
|
+
let trajectories = await this.loadAllTrajectories();
|
|
947
971
|
if (query.status) {
|
|
948
|
-
|
|
972
|
+
trajectories = trajectories.filter((t) => t.status === query.status);
|
|
949
973
|
}
|
|
950
974
|
if (query.since) {
|
|
951
975
|
const sinceTime = new Date(query.since).getTime();
|
|
952
|
-
|
|
953
|
-
(
|
|
976
|
+
trajectories = trajectories.filter(
|
|
977
|
+
(trajectory2) => new Date(trajectory2.startedAt).getTime() >= sinceTime
|
|
954
978
|
);
|
|
955
979
|
}
|
|
956
980
|
if (query.until) {
|
|
957
981
|
const untilTime = new Date(query.until).getTime();
|
|
958
|
-
|
|
959
|
-
(
|
|
982
|
+
trajectories = trajectories.filter(
|
|
983
|
+
(trajectory2) => new Date(trajectory2.startedAt).getTime() <= untilTime
|
|
960
984
|
);
|
|
961
985
|
}
|
|
962
986
|
const sortBy = query.sortBy ?? "startedAt";
|
|
963
987
|
const sortOrder = query.sortOrder ?? "desc";
|
|
964
|
-
|
|
965
|
-
const aVal = a
|
|
966
|
-
const bVal = b
|
|
988
|
+
trajectories.sort((a, b) => {
|
|
989
|
+
const aVal = this.getSortValue(a, sortBy);
|
|
990
|
+
const bVal = this.getSortValue(b, sortBy);
|
|
967
991
|
const cmp = String(aVal).localeCompare(String(bVal));
|
|
968
992
|
return sortOrder === "asc" ? cmp : -cmp;
|
|
969
993
|
});
|
|
970
994
|
const offset = query.offset ?? 0;
|
|
971
995
|
const limit = query.limit ?? 500;
|
|
972
|
-
|
|
973
|
-
return
|
|
974
|
-
entries.map(async ([id, entry]) => {
|
|
975
|
-
const trajectory2 = await this.get(id);
|
|
976
|
-
return {
|
|
977
|
-
id,
|
|
978
|
-
title: entry.title,
|
|
979
|
-
status: entry.status,
|
|
980
|
-
startedAt: entry.startedAt,
|
|
981
|
-
completedAt: entry.completedAt,
|
|
982
|
-
confidence: trajectory2?.retrospective?.confidence,
|
|
983
|
-
chapterCount: trajectory2?.chapters.length ?? 0,
|
|
984
|
-
decisionCount: trajectory2?.chapters.reduce(
|
|
985
|
-
(count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
|
|
986
|
-
0
|
|
987
|
-
) ?? 0
|
|
988
|
-
};
|
|
989
|
-
})
|
|
990
|
-
);
|
|
996
|
+
trajectories = trajectories.slice(offset, offset + limit);
|
|
997
|
+
return trajectories.map((trajectory2) => this.toSummary(trajectory2));
|
|
991
998
|
}
|
|
992
999
|
/**
|
|
993
1000
|
* Delete a trajectory
|
|
994
1001
|
*/
|
|
995
1002
|
async delete(id) {
|
|
996
|
-
|
|
997
|
-
if (existsSync(activePath)) {
|
|
998
|
-
await unlink(activePath);
|
|
999
|
-
}
|
|
1000
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1001
|
-
const index = await this.loadIndex();
|
|
1002
|
-
const entry = index.trajectories[id];
|
|
1003
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
1004
|
-
await unlink(entry.path);
|
|
1005
|
-
const mdPath = entry.path.replace(".json", ".md");
|
|
1006
|
-
if (existsSync(mdPath)) {
|
|
1007
|
-
await unlink(mdPath);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
delete index.trajectories[id];
|
|
1011
|
-
await this.saveIndex(index);
|
|
1012
|
-
});
|
|
1003
|
+
await this.deleteWithSummary(id);
|
|
1013
1004
|
}
|
|
1014
1005
|
/**
|
|
1015
1006
|
* Search trajectories by text
|
|
@@ -1042,12 +1033,395 @@ var FileStorage = class {
|
|
|
1042
1033
|
}
|
|
1043
1034
|
return matches;
|
|
1044
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Mark a trajectory as compacted without writing to a shared index.
|
|
1038
|
+
*/
|
|
1039
|
+
async markCompacted(id, compactedInto) {
|
|
1040
|
+
const markedIds = await this.markCompactedMany([id], compactedInto);
|
|
1041
|
+
return markedIds.has(id);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Mark multiple trajectories as compacted with one filesystem scan.
|
|
1045
|
+
*/
|
|
1046
|
+
async markCompactedMany(ids, compactedInto) {
|
|
1047
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
1048
|
+
const markedIds = /* @__PURE__ */ new Set();
|
|
1049
|
+
const compactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1050
|
+
const writes = [];
|
|
1051
|
+
for (const [id, paths] of pathsById.entries()) {
|
|
1052
|
+
if (paths.length === 0) {
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
markedIds.add(id);
|
|
1056
|
+
const marker = {
|
|
1057
|
+
trajectoryId: id,
|
|
1058
|
+
compactedInto,
|
|
1059
|
+
compactedAt
|
|
1060
|
+
};
|
|
1061
|
+
for (const filePath of paths) {
|
|
1062
|
+
writes.push(
|
|
1063
|
+
writeFile(
|
|
1064
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
1065
|
+
JSON.stringify(marker, null, 2),
|
|
1066
|
+
"utf-8"
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
await Promise.all(writes);
|
|
1072
|
+
return markedIds;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Return trajectory IDs that have a per-trajectory compaction marker.
|
|
1076
|
+
*/
|
|
1077
|
+
async getCompactedTrajectoryIds() {
|
|
1078
|
+
const markerPaths = await this.listCompactionMarkerFiles();
|
|
1079
|
+
const compactedIds = /* @__PURE__ */ new Set();
|
|
1080
|
+
for (const markerPath of markerPaths) {
|
|
1081
|
+
try {
|
|
1082
|
+
const marker = JSON.parse(
|
|
1083
|
+
await readFile(markerPath, "utf-8")
|
|
1084
|
+
);
|
|
1085
|
+
const trajectoryId = typeof marker.trajectoryId === "string" ? marker.trajectoryId : this.getTrajectoryIdFromCompactionMarkerPath(markerPath);
|
|
1086
|
+
if (trajectoryId && typeof marker.compactedInto === "string") {
|
|
1087
|
+
compactedIds.add(trajectoryId);
|
|
1088
|
+
}
|
|
1089
|
+
} catch {
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return compactedIds;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Delete a trajectory and return file counts for CLI reporting.
|
|
1096
|
+
*/
|
|
1097
|
+
async deleteWithSummary(id) {
|
|
1098
|
+
return this.deleteManyWithSummary([id]);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Delete multiple trajectories with one filesystem scan.
|
|
1102
|
+
*/
|
|
1103
|
+
async deleteManyWithSummary(ids) {
|
|
1104
|
+
const summary = {
|
|
1105
|
+
removedTrajectories: 0,
|
|
1106
|
+
deletedJsonFiles: 0,
|
|
1107
|
+
deletedMarkdownFiles: 0,
|
|
1108
|
+
deletedTraceFiles: 0,
|
|
1109
|
+
deletedCompactionFiles: 0
|
|
1110
|
+
};
|
|
1111
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
1112
|
+
const deletedPaths = /* @__PURE__ */ new Set();
|
|
1113
|
+
for (const paths of pathsById.values()) {
|
|
1114
|
+
for (const filePath of paths) {
|
|
1115
|
+
if (deletedPaths.has(filePath)) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
deletedPaths.add(filePath);
|
|
1119
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return summary;
|
|
1123
|
+
}
|
|
1045
1124
|
/**
|
|
1046
1125
|
* Close storage (no-op for file storage)
|
|
1047
1126
|
*/
|
|
1048
1127
|
async close() {
|
|
1049
1128
|
}
|
|
1050
1129
|
// Private helpers
|
|
1130
|
+
getActiveCandidatePaths(id) {
|
|
1131
|
+
if (!isSafeTrajectoryId(id)) {
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
return [
|
|
1135
|
+
join(this.activeDir, id, TRAJECTORY_FILE),
|
|
1136
|
+
// Legacy layout from v0.5.x and earlier.
|
|
1137
|
+
join(this.activeDir, `${id}.json`)
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
async loadAllTrajectories() {
|
|
1141
|
+
const files = await this.listTrajectoryFiles();
|
|
1142
|
+
const trajectories = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const filePath of files) {
|
|
1144
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
1145
|
+
if (!trajectory2) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const current = trajectories.get(trajectory2.id);
|
|
1149
|
+
if (!current || this.isNewerTrajectory(trajectory2, current)) {
|
|
1150
|
+
trajectories.set(trajectory2.id, trajectory2);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return Array.from(trajectories.values());
|
|
1154
|
+
}
|
|
1155
|
+
async listTrajectoryFiles() {
|
|
1156
|
+
const [activeFiles, completedFiles] = await Promise.all([
|
|
1157
|
+
this.collectTrajectoryFiles(this.activeDir),
|
|
1158
|
+
this.collectTrajectoryFiles(this.completedDir)
|
|
1159
|
+
]);
|
|
1160
|
+
return [...activeFiles, ...completedFiles];
|
|
1161
|
+
}
|
|
1162
|
+
async collectTrajectoryFiles(dir) {
|
|
1163
|
+
const files = [];
|
|
1164
|
+
await this.walkJsonFilesInto(dir, files);
|
|
1165
|
+
return files;
|
|
1166
|
+
}
|
|
1167
|
+
async findTrajectoryFilePaths(id) {
|
|
1168
|
+
const pathsById = await this.findTrajectoryFilePathsForIds([id]);
|
|
1169
|
+
return pathsById.get(id) ?? [];
|
|
1170
|
+
}
|
|
1171
|
+
async findTrajectoryFilePathsForIds(ids) {
|
|
1172
|
+
const targetIds = new Set(Array.from(ids).filter(isSafeTrajectoryId));
|
|
1173
|
+
const pathsById = new Map(
|
|
1174
|
+
Array.from(targetIds).map((id) => [id, []])
|
|
1175
|
+
);
|
|
1176
|
+
if (targetIds.size === 0) {
|
|
1177
|
+
return pathsById;
|
|
1178
|
+
}
|
|
1179
|
+
const allFiles = await this.listTrajectoryFiles();
|
|
1180
|
+
for (const filePath of allFiles) {
|
|
1181
|
+
const trajectoryId = this.getTrajectoryIdFromPath(filePath);
|
|
1182
|
+
if (!trajectoryId || !targetIds.has(trajectoryId)) {
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
pathsById.get(trajectoryId)?.push(filePath);
|
|
1186
|
+
}
|
|
1187
|
+
return pathsById;
|
|
1188
|
+
}
|
|
1189
|
+
getTrajectoryIdFromPath(filePath) {
|
|
1190
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1191
|
+
const id = basename(dirname(filePath));
|
|
1192
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1193
|
+
}
|
|
1194
|
+
const name = basename(filePath);
|
|
1195
|
+
if (name.endsWith(".json")) {
|
|
1196
|
+
const id = name.slice(0, -".json".length);
|
|
1197
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1198
|
+
}
|
|
1199
|
+
return void 0;
|
|
1200
|
+
}
|
|
1201
|
+
async removeTrajectoryFiles(paths, exceptPath) {
|
|
1202
|
+
const summary = this.emptyDeleteSummary();
|
|
1203
|
+
for (const filePath of paths) {
|
|
1204
|
+
if (filePath === exceptPath) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async removeTrajectoryFile(filePath, summary) {
|
|
1211
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1212
|
+
const trajectoryDir = dirname(filePath);
|
|
1213
|
+
await this.countDirectoryTrajectoryFiles(trajectoryDir, summary);
|
|
1214
|
+
await this.removeFileIfExists(
|
|
1215
|
+
join(dirname(trajectoryDir), `${basename(trajectoryDir)}.trace.json`),
|
|
1216
|
+
"trace",
|
|
1217
|
+
summary
|
|
1218
|
+
);
|
|
1219
|
+
await rm(trajectoryDir, { recursive: true, force: true });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
await this.removeFileIfExists(filePath, "json", summary);
|
|
1223
|
+
await this.removeFileIfExists(
|
|
1224
|
+
getMarkdownOutputPath(filePath),
|
|
1225
|
+
"markdown",
|
|
1226
|
+
summary
|
|
1227
|
+
);
|
|
1228
|
+
await this.removeFileIfExists(
|
|
1229
|
+
getTraceOutputPath(filePath),
|
|
1230
|
+
"trace",
|
|
1231
|
+
summary
|
|
1232
|
+
);
|
|
1233
|
+
await this.removeFileIfExists(
|
|
1234
|
+
getLegacyCompactionMarkerPath(filePath),
|
|
1235
|
+
"compaction",
|
|
1236
|
+
summary
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
async countDirectoryTrajectoryFiles(trajectoryDir, summary) {
|
|
1240
|
+
await this.countFileIfExists(
|
|
1241
|
+
join(trajectoryDir, TRAJECTORY_FILE),
|
|
1242
|
+
"json",
|
|
1243
|
+
summary
|
|
1244
|
+
);
|
|
1245
|
+
await this.countFileIfExists(
|
|
1246
|
+
join(trajectoryDir, SUMMARY_FILE),
|
|
1247
|
+
"markdown",
|
|
1248
|
+
summary
|
|
1249
|
+
);
|
|
1250
|
+
await this.countFileIfExists(
|
|
1251
|
+
join(trajectoryDir, `${basename(trajectoryDir)}.trace.json`),
|
|
1252
|
+
"trace",
|
|
1253
|
+
summary
|
|
1254
|
+
);
|
|
1255
|
+
await this.countFileIfExists(
|
|
1256
|
+
join(trajectoryDir, "trace.json"),
|
|
1257
|
+
"trace",
|
|
1258
|
+
summary
|
|
1259
|
+
);
|
|
1260
|
+
await this.countFileIfExists(
|
|
1261
|
+
join(trajectoryDir, COMPACTION_FILE),
|
|
1262
|
+
"compaction",
|
|
1263
|
+
summary
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
async removeFileIfExists(path, kind, summary) {
|
|
1267
|
+
if (!existsSync(path)) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
await rm(path, { force: true });
|
|
1271
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1272
|
+
}
|
|
1273
|
+
async countFileIfExists(path, kind, summary) {
|
|
1274
|
+
if (existsSync(path)) {
|
|
1275
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
incrementDeleteSummary(kind, summary) {
|
|
1279
|
+
if (kind === "json") {
|
|
1280
|
+
summary.deletedJsonFiles += 1;
|
|
1281
|
+
summary.removedTrajectories += 1;
|
|
1282
|
+
} else if (kind === "markdown") {
|
|
1283
|
+
summary.deletedMarkdownFiles += 1;
|
|
1284
|
+
} else if (kind === "trace") {
|
|
1285
|
+
summary.deletedTraceFiles += 1;
|
|
1286
|
+
} else {
|
|
1287
|
+
summary.deletedCompactionFiles += 1;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
emptyDeleteSummary() {
|
|
1291
|
+
return {
|
|
1292
|
+
removedTrajectories: 0,
|
|
1293
|
+
deletedJsonFiles: 0,
|
|
1294
|
+
deletedMarkdownFiles: 0,
|
|
1295
|
+
deletedTraceFiles: 0,
|
|
1296
|
+
deletedCompactionFiles: 0
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
async listCompactionMarkerFiles() {
|
|
1300
|
+
const markerPaths = [];
|
|
1301
|
+
await this.walkFilesInto(
|
|
1302
|
+
this.activeDir,
|
|
1303
|
+
markerPaths,
|
|
1304
|
+
isCompactionMarkerFile
|
|
1305
|
+
);
|
|
1306
|
+
await this.walkFilesInto(
|
|
1307
|
+
this.completedDir,
|
|
1308
|
+
markerPaths,
|
|
1309
|
+
isCompactionMarkerFile
|
|
1310
|
+
);
|
|
1311
|
+
return markerPaths;
|
|
1312
|
+
}
|
|
1313
|
+
getCompactionMarkerPath(filePath, id) {
|
|
1314
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1315
|
+
return join(dirname(filePath), COMPACTION_FILE);
|
|
1316
|
+
}
|
|
1317
|
+
return join(dirname(filePath), `${id}${LEGACY_COMPACTION_SUFFIX}`);
|
|
1318
|
+
}
|
|
1319
|
+
getTrajectoryIdFromCompactionMarkerPath(markerPath) {
|
|
1320
|
+
if (basename(markerPath) === COMPACTION_FILE) {
|
|
1321
|
+
const id = basename(dirname(markerPath));
|
|
1322
|
+
return id.startsWith("traj_") ? id : void 0;
|
|
1323
|
+
}
|
|
1324
|
+
const markerName = basename(markerPath);
|
|
1325
|
+
return markerName.endsWith(LEGACY_COMPACTION_SUFFIX) ? markerName.slice(0, -LEGACY_COMPACTION_SUFFIX.length) : void 0;
|
|
1326
|
+
}
|
|
1327
|
+
async migrateLegacyIndexCompactionMarkers() {
|
|
1328
|
+
const indexPath = join(this.trajectoriesDir, "index.json");
|
|
1329
|
+
if (!existsSync(indexPath)) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
let parsed;
|
|
1333
|
+
try {
|
|
1334
|
+
parsed = JSON.parse(await readFile(indexPath, "utf-8"));
|
|
1335
|
+
} catch {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const trajectories = parsed.trajectories;
|
|
1342
|
+
if (trajectories === null || typeof trajectories !== "object" || Array.isArray(trajectories)) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
await Promise.all(
|
|
1346
|
+
Object.entries(trajectories).map(async ([id, entry]) => {
|
|
1347
|
+
if (entry === null || typeof entry !== "object" || !isSafeTrajectoryId(id)) {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const compactedInto = entry.compactedInto;
|
|
1351
|
+
const path = entry.path;
|
|
1352
|
+
if (typeof compactedInto !== "string") {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const paths = typeof path === "string" && existsSync(path) && this.isPathInsideTrajectoriesDir(path) ? [path] : await this.findTrajectoryFilePaths(id);
|
|
1356
|
+
if (paths.length === 0) return;
|
|
1357
|
+
const marker = {
|
|
1358
|
+
trajectoryId: id,
|
|
1359
|
+
compactedInto,
|
|
1360
|
+
compactedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1361
|
+
};
|
|
1362
|
+
await Promise.all(
|
|
1363
|
+
paths.map(
|
|
1364
|
+
(filePath) => writeFile(
|
|
1365
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
1366
|
+
JSON.stringify(marker, null, 2),
|
|
1367
|
+
"utf-8"
|
|
1368
|
+
)
|
|
1369
|
+
)
|
|
1370
|
+
);
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
isPathInsideTrajectoriesDir(path) {
|
|
1375
|
+
const rel = relative(resolve(this.trajectoriesDir), resolve(path));
|
|
1376
|
+
return Boolean(rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
1377
|
+
}
|
|
1378
|
+
async walkFilesInto(dir, out, predicate) {
|
|
1379
|
+
let entries;
|
|
1380
|
+
try {
|
|
1381
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (error.code === "ENOENT") return;
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
for (const entry of entries) {
|
|
1387
|
+
const entryPath = join(dir, entry.name);
|
|
1388
|
+
if (entry.isDirectory()) {
|
|
1389
|
+
await this.walkFilesInto(entryPath, out, predicate);
|
|
1390
|
+
} else if (entry.isFile() && predicate(entry.name)) {
|
|
1391
|
+
out.push(entryPath);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
getSortValue(trajectory2, sortBy) {
|
|
1396
|
+
if (sortBy === "title") {
|
|
1397
|
+
return trajectory2.task.title;
|
|
1398
|
+
}
|
|
1399
|
+
return trajectory2[sortBy] ?? "";
|
|
1400
|
+
}
|
|
1401
|
+
toSummary(trajectory2) {
|
|
1402
|
+
return {
|
|
1403
|
+
id: trajectory2.id,
|
|
1404
|
+
title: trajectory2.task.title,
|
|
1405
|
+
status: trajectory2.status,
|
|
1406
|
+
startedAt: trajectory2.startedAt,
|
|
1407
|
+
completedAt: trajectory2.completedAt,
|
|
1408
|
+
confidence: trajectory2.retrospective?.confidence,
|
|
1409
|
+
chapterCount: trajectory2.chapters.length,
|
|
1410
|
+
decisionCount: trajectory2.chapters.reduce(
|
|
1411
|
+
(count, chapter) => count + chapter.events.filter((event) => event.type === "decision").length,
|
|
1412
|
+
0
|
|
1413
|
+
)
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
isNewerTrajectory(candidate, current) {
|
|
1417
|
+
const candidateTime = new Date(
|
|
1418
|
+
candidate.completedAt ?? candidate.startedAt
|
|
1419
|
+
).getTime();
|
|
1420
|
+
const currentTime = new Date(
|
|
1421
|
+
current.completedAt ?? current.startedAt
|
|
1422
|
+
).getTime();
|
|
1423
|
+
return candidateTime > currentTime;
|
|
1424
|
+
}
|
|
1051
1425
|
/**
|
|
1052
1426
|
* Read a trajectory file and return a tagged result so callers can
|
|
1053
1427
|
* distinguish missing files, malformed JSON, and schema violations.
|
|
@@ -1087,82 +1461,25 @@ var FileStorage = class {
|
|
|
1087
1461
|
const result = await this.readTrajectoryFile(path);
|
|
1088
1462
|
return result.ok ? result.trajectory : null;
|
|
1089
1463
|
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Read and parse the on-disk index.
|
|
1092
|
-
*
|
|
1093
|
-
* Tolerances (belt-and-braces against the read/write race):
|
|
1094
|
-
* - ENOENT: first-run, return an empty index silently.
|
|
1095
|
-
* - Empty file: a concurrent writer truncated index.json in "w" mode
|
|
1096
|
-
* right before we read. Return an empty index silently — this is
|
|
1097
|
-
* not a real corruption, just an interleaving the mutex + atomic
|
|
1098
|
-
* rename should already prevent. Logging here would be noise.
|
|
1099
|
-
* - Non-empty but malformed JSON: genuinely corrupted on disk (hand
|
|
1100
|
-
* edit, disk error, etc). Log it and return an empty index so the
|
|
1101
|
-
* caller can recover, but keep the log so the problem is visible.
|
|
1102
|
-
*/
|
|
1103
|
-
async loadIndex() {
|
|
1104
|
-
let content;
|
|
1105
|
-
try {
|
|
1106
|
-
content = await readFile(this.indexPath, "utf-8");
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
if (error.code !== "ENOENT") {
|
|
1109
|
-
console.error(
|
|
1110
|
-
"Error loading trajectory index, using empty index:",
|
|
1111
|
-
error
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
return this.emptyIndex();
|
|
1115
|
-
}
|
|
1116
|
-
if (content.length === 0) {
|
|
1117
|
-
return this.emptyIndex();
|
|
1118
|
-
}
|
|
1119
|
-
try {
|
|
1120
|
-
return JSON.parse(content);
|
|
1121
|
-
} catch (error) {
|
|
1122
|
-
console.error(
|
|
1123
|
-
"Error loading trajectory index, using empty index:",
|
|
1124
|
-
error
|
|
1125
|
-
);
|
|
1126
|
-
return this.emptyIndex();
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
emptyIndex() {
|
|
1130
|
-
return {
|
|
1131
|
-
version: 1,
|
|
1132
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1133
|
-
trajectories: {}
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
/**
|
|
1137
|
-
* Atomic write: stage into a process-unique temp path in the same directory
|
|
1138
|
-
* and then rename over the live file. `rename` is atomic on POSIX, so
|
|
1139
|
-
* concurrent readers in any process either see the old complete file or
|
|
1140
|
-
* the new complete file — never a half-written / zero-byte state.
|
|
1141
|
-
*
|
|
1142
|
-
* Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
|
|
1143
|
-
* read-modify-write cycle stays serialized; the unique temp name also keeps
|
|
1144
|
-
* parallel writers in other processes from colliding on a shared tmp path.
|
|
1145
|
-
*/
|
|
1146
|
-
async saveIndex(index) {
|
|
1147
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1148
|
-
const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1149
|
-
await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
|
|
1150
|
-
await rename(tmpPath, this.indexPath);
|
|
1151
|
-
}
|
|
1152
|
-
async updateIndex(trajectory2, filePath) {
|
|
1153
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1154
|
-
const index = await this.loadIndex();
|
|
1155
|
-
index.trajectories[trajectory2.id] = {
|
|
1156
|
-
title: trajectory2.task.title,
|
|
1157
|
-
status: trajectory2.status,
|
|
1158
|
-
startedAt: trajectory2.startedAt,
|
|
1159
|
-
completedAt: trajectory2.completedAt,
|
|
1160
|
-
path: filePath
|
|
1161
|
-
};
|
|
1162
|
-
await this.saveIndex(index);
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
1464
|
};
|
|
1465
|
+
function isTrajectoryJsonFile(name) {
|
|
1466
|
+
return name === TRAJECTORY_FILE || name.endsWith(".json") && name !== "index.json" && !name.endsWith(".trace.json") && !name.endsWith(LEGACY_COMPACTION_SUFFIX) && name !== COMPACTION_FILE;
|
|
1467
|
+
}
|
|
1468
|
+
function isSafeTrajectoryId(id) {
|
|
1469
|
+
return id.length > 0 && !id.includes("..") && !id.includes("/") && !id.includes("\\");
|
|
1470
|
+
}
|
|
1471
|
+
function isCompactionMarkerFile(name) {
|
|
1472
|
+
return name === COMPACTION_FILE || name.endsWith(LEGACY_COMPACTION_SUFFIX);
|
|
1473
|
+
}
|
|
1474
|
+
function getMarkdownOutputPath(outputPath) {
|
|
1475
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
1476
|
+
}
|
|
1477
|
+
function getTraceOutputPath(outputPath) {
|
|
1478
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
|
|
1479
|
+
}
|
|
1480
|
+
function getLegacyCompactionMarkerPath(outputPath) {
|
|
1481
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(LEGACY_COMPACTION_SUFFIX) : `${outputPath}${LEGACY_COMPACTION_SUFFIX}`;
|
|
1482
|
+
}
|
|
1166
1483
|
|
|
1167
1484
|
// src/sdk/client.ts
|
|
1168
1485
|
var require2 = createRequire(import.meta.url);
|
|
@@ -1204,7 +1521,7 @@ function resolveTrajectoryCliInvocation() {
|
|
|
1204
1521
|
);
|
|
1205
1522
|
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.trail ?? (pkg.name ? pkg.bin?.[pkg.name] : void 0);
|
|
1206
1523
|
if (binEntry) {
|
|
1207
|
-
const cliPath = resolvePath(
|
|
1524
|
+
const cliPath = resolvePath(dirname2(packageJsonPath), binEntry);
|
|
1208
1525
|
if (existsSync2(cliPath)) {
|
|
1209
1526
|
return { command: process.execPath, args: [cliPath] };
|
|
1210
1527
|
}
|
|
@@ -1250,7 +1567,7 @@ async function compactWorkflow(workflowId, options) {
|
|
|
1250
1567
|
if (options?.discardSources) {
|
|
1251
1568
|
args.push("--discard-sources");
|
|
1252
1569
|
}
|
|
1253
|
-
return new Promise((
|
|
1570
|
+
return new Promise((resolve2, reject) => {
|
|
1254
1571
|
const child = spawn(cli.command, args, {
|
|
1255
1572
|
cwd: options?.cwd,
|
|
1256
1573
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1278,7 +1595,7 @@ async function compactWorkflow(workflowId, options) {
|
|
|
1278
1595
|
}
|
|
1279
1596
|
try {
|
|
1280
1597
|
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
1281
|
-
|
|
1598
|
+
resolve2(parseCompactWorkflowOutput(stdout));
|
|
1282
1599
|
} catch (error) {
|
|
1283
1600
|
reject(
|
|
1284
1601
|
error instanceof Error ? error : new Error("compactWorkflow failed: unable to parse CLI output")
|
|
@@ -2103,4 +2420,4 @@ export {
|
|
|
2103
2420
|
getCommitsBetween,
|
|
2104
2421
|
getFilesChangedBetween
|
|
2105
2422
|
};
|
|
2106
|
-
//# sourceMappingURL=chunk-
|
|
2423
|
+
//# sourceMappingURL=chunk-JMH3Z5BB.js.map
|