agent-trajectories 0.5.8 → 0.6.0
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 +18 -10
- package/dist/{chunk-WMJRBQB4.js → chunk-ENWKFNUD.js} +522 -267
- package/dist/chunk-ENWKFNUD.js.map +1 -0
- package/dist/cli/index.js +588 -384
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-C9IcYSNQ.d.ts → index-DEr3Rs32.d.ts} +67 -41
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/package.json +3 -1
- package/dist/chunk-WMJRBQB4.js.map +0 -1
|
@@ -666,17 +666,35 @@ 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";
|
|
690
|
+
var DEFAULT_TRAJECTORY_DATA_DIR = join(
|
|
691
|
+
".agentworkforce",
|
|
692
|
+
"trajectories"
|
|
693
|
+
);
|
|
694
|
+
var LEGACY_TRAJECTORY_DATA_DIR = ".trajectories";
|
|
695
|
+
function getDefaultTrajectoryDataDir(baseDir = process.cwd()) {
|
|
696
|
+
return join(baseDir, DEFAULT_TRAJECTORY_DATA_DIR);
|
|
697
|
+
}
|
|
680
698
|
function expandPath(path) {
|
|
681
699
|
if (path.startsWith("~")) {
|
|
682
700
|
return join(process.env.HOME ?? "", path.slice(1));
|
|
@@ -697,64 +715,60 @@ function describeReadFailure(reason, error) {
|
|
|
697
715
|
if (error instanceof Error) return error.message;
|
|
698
716
|
return String(error);
|
|
699
717
|
}
|
|
700
|
-
var indexLocks = /* @__PURE__ */ new Map();
|
|
701
|
-
function withIndexLock(path, task) {
|
|
702
|
-
const prev = indexLocks.get(path) ?? Promise.resolve();
|
|
703
|
-
const next = prev.then(task, task);
|
|
704
|
-
indexLocks.set(
|
|
705
|
-
path,
|
|
706
|
-
next.catch(() => void 0)
|
|
707
|
-
);
|
|
708
|
-
return next;
|
|
709
|
-
}
|
|
710
718
|
var FileStorage = class {
|
|
711
719
|
baseDir;
|
|
712
720
|
trajectoriesDir;
|
|
713
721
|
activeDir;
|
|
714
722
|
completedDir;
|
|
715
|
-
indexPath;
|
|
716
723
|
lastReconcileSummary;
|
|
724
|
+
shouldMigrateLegacyDefault = false;
|
|
717
725
|
constructor(baseDir) {
|
|
718
726
|
this.baseDir = baseDir ?? process.cwd();
|
|
719
727
|
const dataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
720
728
|
if (dataDir) {
|
|
721
729
|
this.trajectoriesDir = expandPath(dataDir);
|
|
722
730
|
} else {
|
|
723
|
-
this.trajectoriesDir =
|
|
731
|
+
this.trajectoriesDir = getDefaultTrajectoryDataDir(this.baseDir);
|
|
732
|
+
this.shouldMigrateLegacyDefault = true;
|
|
724
733
|
}
|
|
725
734
|
this.activeDir = join(this.trajectoriesDir, "active");
|
|
726
735
|
this.completedDir = join(this.trajectoriesDir, "completed");
|
|
727
|
-
this.indexPath = join(this.trajectoriesDir, "index.json");
|
|
728
736
|
}
|
|
729
737
|
/**
|
|
730
738
|
* Initialize storage directories
|
|
731
739
|
*/
|
|
732
740
|
async initialize() {
|
|
741
|
+
await this.migrateLegacyDefaultDir();
|
|
733
742
|
await mkdir(this.trajectoriesDir, { recursive: true });
|
|
734
743
|
await mkdir(this.activeDir, { recursive: true });
|
|
735
744
|
await mkdir(this.completedDir, { recursive: true });
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!existsSync(this.indexPath)) {
|
|
739
|
-
await this.saveIndex(this.emptyIndex());
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
}
|
|
745
|
+
await this.migrateLegacyIndexCompactionMarkers();
|
|
746
|
+
await rm(join(this.trajectoriesDir, "index.json"), { force: true });
|
|
743
747
|
await this.reconcileIndex();
|
|
744
748
|
}
|
|
749
|
+
async migrateLegacyDefaultDir() {
|
|
750
|
+
if (!this.shouldMigrateLegacyDefault) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const legacyDir = join(this.baseDir, LEGACY_TRAJECTORY_DATA_DIR);
|
|
754
|
+
if (!existsSync(legacyDir) || existsSync(this.trajectoriesDir)) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
await mkdir(dirname(this.trajectoriesDir), { recursive: true });
|
|
758
|
+
await rename(legacyDir, this.trajectoriesDir);
|
|
759
|
+
}
|
|
745
760
|
/**
|
|
746
|
-
* Scan active/ and completed/ recursively and
|
|
747
|
-
*
|
|
748
|
-
* only adds, never removes.
|
|
761
|
+
* Scan active/ and completed/ recursively and report trajectory files
|
|
762
|
+
* that can be loaded plus files that should be surfaced by doctor.
|
|
749
763
|
*
|
|
750
764
|
* Handles three on-disk layouts in completed/:
|
|
751
765
|
* - flat: completed/{id}.json (legacy workforce data)
|
|
752
|
-
* - monthly: completed/YYYY-MM/{id}.json (
|
|
766
|
+
* - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
|
|
767
|
+
* - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
|
|
753
768
|
* - nested: completed/.../{id}.json (defensive — any depth)
|
|
754
769
|
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* added.
|
|
770
|
+
* The method name is kept for callers such as `trail doctor`, but no
|
|
771
|
+
* shared index file is written.
|
|
758
772
|
*/
|
|
759
773
|
async reconcileIndex() {
|
|
760
774
|
const summary = {
|
|
@@ -766,58 +780,29 @@ var FileStorage = class {
|
|
|
766
780
|
skippedIoError: 0,
|
|
767
781
|
failures: []
|
|
768
782
|
};
|
|
769
|
-
await
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
783
|
+
const discovered = await this.listTrajectoryFiles();
|
|
784
|
+
for (const filePath of discovered) {
|
|
785
|
+
summary.scanned += 1;
|
|
786
|
+
const result = await this.readTrajectoryFile(filePath);
|
|
787
|
+
if (!result.ok) {
|
|
788
|
+
if (result.reason === "malformed_json") {
|
|
789
|
+
summary.skippedMalformedJson += 1;
|
|
790
|
+
} else if (result.reason === "schema_violation") {
|
|
791
|
+
summary.skippedSchemaViolation += 1;
|
|
792
|
+
} else {
|
|
793
|
+
summary.skippedIoError += 1;
|
|
778
794
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const result = await this.readTrajectoryFile(filePath);
|
|
786
|
-
if (!result.ok) {
|
|
787
|
-
if (result.reason === "malformed_json") {
|
|
788
|
-
summary.skippedMalformedJson += 1;
|
|
789
|
-
} else if (result.reason === "schema_violation") {
|
|
790
|
-
summary.skippedSchemaViolation += 1;
|
|
791
|
-
} else {
|
|
792
|
-
summary.skippedIoError += 1;
|
|
793
|
-
}
|
|
794
|
-
summary.failures.push({
|
|
795
|
-
path: result.path,
|
|
796
|
-
reason: result.reason,
|
|
797
|
-
message: describeReadFailure(result.reason, result.error)
|
|
798
|
-
});
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
const trajectory2 = result.trajectory;
|
|
802
|
-
if (index.trajectories[trajectory2.id]) {
|
|
803
|
-
summary.alreadyIndexed += 1;
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
index.trajectories[trajectory2.id] = {
|
|
807
|
-
title: trajectory2.task.title,
|
|
808
|
-
status: trajectory2.status,
|
|
809
|
-
startedAt: trajectory2.startedAt,
|
|
810
|
-
completedAt: trajectory2.completedAt,
|
|
811
|
-
path: filePath
|
|
812
|
-
};
|
|
813
|
-
summary.added += 1;
|
|
814
|
-
}
|
|
815
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
816
|
-
await this.saveIndex(index);
|
|
795
|
+
summary.failures.push({
|
|
796
|
+
path: result.path,
|
|
797
|
+
reason: result.reason,
|
|
798
|
+
message: describeReadFailure(result.reason, result.error)
|
|
799
|
+
});
|
|
800
|
+
continue;
|
|
817
801
|
}
|
|
818
|
-
|
|
802
|
+
summary.added += 1;
|
|
803
|
+
}
|
|
819
804
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
820
|
-
if (
|
|
805
|
+
if (hadSkips) {
|
|
821
806
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
822
807
|
if (summary.skippedMalformedJson > 0) {
|
|
823
808
|
parts.push(`malformed: ${summary.skippedMalformedJson}`);
|
|
@@ -842,7 +827,7 @@ var FileStorage = class {
|
|
|
842
827
|
return this.lastReconcileSummary;
|
|
843
828
|
}
|
|
844
829
|
/**
|
|
845
|
-
* Move trajectory files that fail to load into `.trajectories/invalid/`
|
|
830
|
+
* Move trajectory files that fail to load into `.agentworkforce/trajectories/invalid/`
|
|
846
831
|
* so reconcile no longer scans them. Only quarantines parse and schema
|
|
847
832
|
* failures — transient io_error failures are left in place because the
|
|
848
833
|
* file may load fine on the next attempt.
|
|
@@ -899,7 +884,7 @@ var FileStorage = class {
|
|
|
899
884
|
return dest;
|
|
900
885
|
}
|
|
901
886
|
/**
|
|
902
|
-
* Recursively collect
|
|
887
|
+
* Recursively collect trajectory JSON file paths under `dir` into `out`.
|
|
903
888
|
* Silently treats a missing directory as empty.
|
|
904
889
|
*/
|
|
905
890
|
async walkJsonFilesInto(dir, out) {
|
|
@@ -914,7 +899,7 @@ var FileStorage = class {
|
|
|
914
899
|
const entryPath = join(dir, entry.name);
|
|
915
900
|
if (entry.isDirectory()) {
|
|
916
901
|
await this.walkJsonFilesInto(entryPath, out);
|
|
917
|
-
} else if (entry.isFile() && entry.name
|
|
902
|
+
} else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
|
|
918
903
|
out.push(entryPath);
|
|
919
904
|
}
|
|
920
905
|
}
|
|
@@ -938,56 +923,43 @@ var FileStorage = class {
|
|
|
938
923
|
}
|
|
939
924
|
const trajectory2 = validation.data;
|
|
940
925
|
const isCompleted = trajectory2.status === "completed" || trajectory2.status === "abandoned";
|
|
941
|
-
|
|
926
|
+
const existingPaths = await this.findTrajectoryFilePaths(trajectory2.id);
|
|
927
|
+
let trajectoryDir;
|
|
942
928
|
if (isCompleted) {
|
|
943
929
|
const date = new Date(trajectory2.completedAt ?? trajectory2.startedAt);
|
|
944
930
|
const monthDir = join(
|
|
945
931
|
this.completedDir,
|
|
946
932
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
947
933
|
);
|
|
948
|
-
|
|
949
|
-
filePath = join(monthDir, `${trajectory2.id}.json`);
|
|
950
|
-
const activePath = join(this.activeDir, `${trajectory2.id}.json`);
|
|
951
|
-
if (existsSync(activePath)) {
|
|
952
|
-
await unlink(activePath);
|
|
953
|
-
}
|
|
954
|
-
const mdPath = join(monthDir, `${trajectory2.id}.md`);
|
|
955
|
-
const markdown = exportToMarkdown(trajectory2);
|
|
956
|
-
await writeFile(mdPath, markdown, "utf-8");
|
|
934
|
+
trajectoryDir = join(monthDir, trajectory2.id);
|
|
957
935
|
} else {
|
|
958
|
-
|
|
936
|
+
trajectoryDir = join(this.activeDir, trajectory2.id);
|
|
937
|
+
}
|
|
938
|
+
const filePath = join(trajectoryDir, TRAJECTORY_FILE);
|
|
939
|
+
await this.removeTrajectoryFiles(existingPaths, filePath);
|
|
940
|
+
await mkdir(trajectoryDir, { recursive: true });
|
|
941
|
+
if (isCompleted) {
|
|
942
|
+
const markdown = exportToMarkdown(trajectory2);
|
|
943
|
+
await writeFile(join(trajectoryDir, SUMMARY_FILE), markdown, "utf-8");
|
|
959
944
|
}
|
|
960
945
|
await writeFile(filePath, JSON.stringify(trajectory2, null, 2), "utf-8");
|
|
961
|
-
await this.updateIndex(trajectory2, filePath);
|
|
962
946
|
}
|
|
963
947
|
/**
|
|
964
948
|
* Get a trajectory by ID
|
|
965
949
|
*/
|
|
966
950
|
async get(id) {
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const entry = index.trajectories[id];
|
|
973
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
974
|
-
return this.readTrajectoryOrNull(entry.path);
|
|
975
|
-
}
|
|
976
|
-
try {
|
|
977
|
-
const flatPath = join(this.completedDir, `${id}.json`);
|
|
978
|
-
if (existsSync(flatPath)) {
|
|
979
|
-
return this.readTrajectoryOrNull(flatPath);
|
|
980
|
-
}
|
|
981
|
-
const months = await readdir(this.completedDir);
|
|
982
|
-
for (const month of months) {
|
|
983
|
-
const filePath = join(this.completedDir, month, `${id}.json`);
|
|
984
|
-
if (existsSync(filePath)) {
|
|
985
|
-
return this.readTrajectoryOrNull(filePath);
|
|
986
|
-
}
|
|
951
|
+
for (const filePath of this.getActiveCandidatePaths(id)) {
|
|
952
|
+
if (!existsSync(filePath)) continue;
|
|
953
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
954
|
+
if (trajectory2?.id === id) {
|
|
955
|
+
return trajectory2;
|
|
987
956
|
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
|
|
957
|
+
}
|
|
958
|
+
const paths = await this.findTrajectoryFilePaths(id);
|
|
959
|
+
for (const filePath of paths) {
|
|
960
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
961
|
+
if (trajectory2?.id === id) {
|
|
962
|
+
return trajectory2;
|
|
991
963
|
}
|
|
992
964
|
}
|
|
993
965
|
return null;
|
|
@@ -996,107 +968,61 @@ var FileStorage = class {
|
|
|
996
968
|
* Get the currently active trajectory
|
|
997
969
|
*/
|
|
998
970
|
async getActive() {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
1002
|
-
if (jsonFiles.length === 0) {
|
|
1003
|
-
return null;
|
|
1004
|
-
}
|
|
1005
|
-
let mostRecent = null;
|
|
1006
|
-
let mostRecentTime = 0;
|
|
1007
|
-
for (const file of jsonFiles) {
|
|
1008
|
-
const trajectory2 = await this.readTrajectoryOrNull(
|
|
1009
|
-
join(this.activeDir, file)
|
|
1010
|
-
);
|
|
1011
|
-
if (trajectory2) {
|
|
1012
|
-
const startTime = new Date(trajectory2.startedAt).getTime();
|
|
1013
|
-
if (startTime > mostRecentTime) {
|
|
1014
|
-
mostRecentTime = startTime;
|
|
1015
|
-
mostRecent = trajectory2;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
return mostRecent;
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
if (error.code === "ENOENT") {
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
console.error("Error reading active trajectories:", error);
|
|
971
|
+
const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
|
|
972
|
+
if (activeFiles.length === 0) {
|
|
1025
973
|
return null;
|
|
1026
974
|
}
|
|
975
|
+
let mostRecent = null;
|
|
976
|
+
let mostRecentTime = 0;
|
|
977
|
+
for (const filePath of activeFiles) {
|
|
978
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
979
|
+
if (trajectory2?.status !== "active") continue;
|
|
980
|
+
const startTime = new Date(trajectory2.startedAt).getTime();
|
|
981
|
+
if (startTime > mostRecentTime) {
|
|
982
|
+
mostRecentTime = startTime;
|
|
983
|
+
mostRecent = trajectory2;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return mostRecent;
|
|
1027
987
|
}
|
|
1028
988
|
/**
|
|
1029
989
|
* List trajectories with optional filtering
|
|
1030
990
|
*/
|
|
1031
991
|
async list(query) {
|
|
1032
|
-
|
|
1033
|
-
let entries = Object.entries(index.trajectories);
|
|
992
|
+
let trajectories = await this.loadAllTrajectories();
|
|
1034
993
|
if (query.status) {
|
|
1035
|
-
|
|
994
|
+
trajectories = trajectories.filter((t) => t.status === query.status);
|
|
1036
995
|
}
|
|
1037
996
|
if (query.since) {
|
|
1038
997
|
const sinceTime = new Date(query.since).getTime();
|
|
1039
|
-
|
|
1040
|
-
(
|
|
998
|
+
trajectories = trajectories.filter(
|
|
999
|
+
(trajectory2) => new Date(trajectory2.startedAt).getTime() >= sinceTime
|
|
1041
1000
|
);
|
|
1042
1001
|
}
|
|
1043
1002
|
if (query.until) {
|
|
1044
1003
|
const untilTime = new Date(query.until).getTime();
|
|
1045
|
-
|
|
1046
|
-
(
|
|
1004
|
+
trajectories = trajectories.filter(
|
|
1005
|
+
(trajectory2) => new Date(trajectory2.startedAt).getTime() <= untilTime
|
|
1047
1006
|
);
|
|
1048
1007
|
}
|
|
1049
1008
|
const sortBy = query.sortBy ?? "startedAt";
|
|
1050
1009
|
const sortOrder = query.sortOrder ?? "desc";
|
|
1051
|
-
|
|
1052
|
-
const aVal = a
|
|
1053
|
-
const bVal = b
|
|
1010
|
+
trajectories.sort((a, b) => {
|
|
1011
|
+
const aVal = this.getSortValue(a, sortBy);
|
|
1012
|
+
const bVal = this.getSortValue(b, sortBy);
|
|
1054
1013
|
const cmp = String(aVal).localeCompare(String(bVal));
|
|
1055
1014
|
return sortOrder === "asc" ? cmp : -cmp;
|
|
1056
1015
|
});
|
|
1057
1016
|
const offset = query.offset ?? 0;
|
|
1058
1017
|
const limit = query.limit ?? 500;
|
|
1059
|
-
|
|
1060
|
-
return
|
|
1061
|
-
entries.map(async ([id, entry]) => {
|
|
1062
|
-
const trajectory2 = await this.get(id);
|
|
1063
|
-
return {
|
|
1064
|
-
id,
|
|
1065
|
-
title: entry.title,
|
|
1066
|
-
status: entry.status,
|
|
1067
|
-
startedAt: entry.startedAt,
|
|
1068
|
-
completedAt: entry.completedAt,
|
|
1069
|
-
confidence: trajectory2?.retrospective?.confidence,
|
|
1070
|
-
chapterCount: trajectory2?.chapters.length ?? 0,
|
|
1071
|
-
decisionCount: trajectory2?.chapters.reduce(
|
|
1072
|
-
(count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
|
|
1073
|
-
0
|
|
1074
|
-
) ?? 0
|
|
1075
|
-
};
|
|
1076
|
-
})
|
|
1077
|
-
);
|
|
1018
|
+
trajectories = trajectories.slice(offset, offset + limit);
|
|
1019
|
+
return trajectories.map((trajectory2) => this.toSummary(trajectory2));
|
|
1078
1020
|
}
|
|
1079
1021
|
/**
|
|
1080
1022
|
* Delete a trajectory
|
|
1081
1023
|
*/
|
|
1082
1024
|
async delete(id) {
|
|
1083
|
-
|
|
1084
|
-
if (existsSync(activePath)) {
|
|
1085
|
-
await unlink(activePath);
|
|
1086
|
-
}
|
|
1087
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1088
|
-
const index = await this.loadIndex();
|
|
1089
|
-
const entry = index.trajectories[id];
|
|
1090
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
1091
|
-
await unlink(entry.path);
|
|
1092
|
-
const mdPath = entry.path.replace(".json", ".md");
|
|
1093
|
-
if (existsSync(mdPath)) {
|
|
1094
|
-
await unlink(mdPath);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
delete index.trajectories[id];
|
|
1098
|
-
await this.saveIndex(index);
|
|
1099
|
-
});
|
|
1025
|
+
await this.deleteWithSummary(id);
|
|
1100
1026
|
}
|
|
1101
1027
|
/**
|
|
1102
1028
|
* Search trajectories by text
|
|
@@ -1129,12 +1055,395 @@ var FileStorage = class {
|
|
|
1129
1055
|
}
|
|
1130
1056
|
return matches;
|
|
1131
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Mark a trajectory as compacted without writing to a shared index.
|
|
1060
|
+
*/
|
|
1061
|
+
async markCompacted(id, compactedInto) {
|
|
1062
|
+
const markedIds = await this.markCompactedMany([id], compactedInto);
|
|
1063
|
+
return markedIds.has(id);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Mark multiple trajectories as compacted with one filesystem scan.
|
|
1067
|
+
*/
|
|
1068
|
+
async markCompactedMany(ids, compactedInto) {
|
|
1069
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
1070
|
+
const markedIds = /* @__PURE__ */ new Set();
|
|
1071
|
+
const compactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1072
|
+
const writes = [];
|
|
1073
|
+
for (const [id, paths] of pathsById.entries()) {
|
|
1074
|
+
if (paths.length === 0) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
markedIds.add(id);
|
|
1078
|
+
const marker = {
|
|
1079
|
+
trajectoryId: id,
|
|
1080
|
+
compactedInto,
|
|
1081
|
+
compactedAt
|
|
1082
|
+
};
|
|
1083
|
+
for (const filePath of paths) {
|
|
1084
|
+
writes.push(
|
|
1085
|
+
writeFile(
|
|
1086
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
1087
|
+
JSON.stringify(marker, null, 2),
|
|
1088
|
+
"utf-8"
|
|
1089
|
+
)
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
await Promise.all(writes);
|
|
1094
|
+
return markedIds;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Return trajectory IDs that have a per-trajectory compaction marker.
|
|
1098
|
+
*/
|
|
1099
|
+
async getCompactedTrajectoryIds() {
|
|
1100
|
+
const markerPaths = await this.listCompactionMarkerFiles();
|
|
1101
|
+
const compactedIds = /* @__PURE__ */ new Set();
|
|
1102
|
+
for (const markerPath of markerPaths) {
|
|
1103
|
+
try {
|
|
1104
|
+
const marker = JSON.parse(
|
|
1105
|
+
await readFile(markerPath, "utf-8")
|
|
1106
|
+
);
|
|
1107
|
+
const trajectoryId = typeof marker.trajectoryId === "string" ? marker.trajectoryId : this.getTrajectoryIdFromCompactionMarkerPath(markerPath);
|
|
1108
|
+
if (trajectoryId && typeof marker.compactedInto === "string") {
|
|
1109
|
+
compactedIds.add(trajectoryId);
|
|
1110
|
+
}
|
|
1111
|
+
} catch {
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return compactedIds;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Delete a trajectory and return file counts for CLI reporting.
|
|
1118
|
+
*/
|
|
1119
|
+
async deleteWithSummary(id) {
|
|
1120
|
+
return this.deleteManyWithSummary([id]);
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Delete multiple trajectories with one filesystem scan.
|
|
1124
|
+
*/
|
|
1125
|
+
async deleteManyWithSummary(ids) {
|
|
1126
|
+
const summary = {
|
|
1127
|
+
removedTrajectories: 0,
|
|
1128
|
+
deletedJsonFiles: 0,
|
|
1129
|
+
deletedMarkdownFiles: 0,
|
|
1130
|
+
deletedTraceFiles: 0,
|
|
1131
|
+
deletedCompactionFiles: 0
|
|
1132
|
+
};
|
|
1133
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
1134
|
+
const deletedPaths = /* @__PURE__ */ new Set();
|
|
1135
|
+
for (const paths of pathsById.values()) {
|
|
1136
|
+
for (const filePath of paths) {
|
|
1137
|
+
if (deletedPaths.has(filePath)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
deletedPaths.add(filePath);
|
|
1141
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return summary;
|
|
1145
|
+
}
|
|
1132
1146
|
/**
|
|
1133
1147
|
* Close storage (no-op for file storage)
|
|
1134
1148
|
*/
|
|
1135
1149
|
async close() {
|
|
1136
1150
|
}
|
|
1137
1151
|
// Private helpers
|
|
1152
|
+
getActiveCandidatePaths(id) {
|
|
1153
|
+
if (!isSafeTrajectoryId(id)) {
|
|
1154
|
+
return [];
|
|
1155
|
+
}
|
|
1156
|
+
return [
|
|
1157
|
+
join(this.activeDir, id, TRAJECTORY_FILE),
|
|
1158
|
+
// Legacy layout from v0.5.x and earlier.
|
|
1159
|
+
join(this.activeDir, `${id}.json`)
|
|
1160
|
+
];
|
|
1161
|
+
}
|
|
1162
|
+
async loadAllTrajectories() {
|
|
1163
|
+
const files = await this.listTrajectoryFiles();
|
|
1164
|
+
const trajectories = /* @__PURE__ */ new Map();
|
|
1165
|
+
for (const filePath of files) {
|
|
1166
|
+
const trajectory2 = await this.readTrajectoryOrNull(filePath);
|
|
1167
|
+
if (!trajectory2) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const current = trajectories.get(trajectory2.id);
|
|
1171
|
+
if (!current || this.isNewerTrajectory(trajectory2, current)) {
|
|
1172
|
+
trajectories.set(trajectory2.id, trajectory2);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return Array.from(trajectories.values());
|
|
1176
|
+
}
|
|
1177
|
+
async listTrajectoryFiles() {
|
|
1178
|
+
const [activeFiles, completedFiles] = await Promise.all([
|
|
1179
|
+
this.collectTrajectoryFiles(this.activeDir),
|
|
1180
|
+
this.collectTrajectoryFiles(this.completedDir)
|
|
1181
|
+
]);
|
|
1182
|
+
return [...activeFiles, ...completedFiles];
|
|
1183
|
+
}
|
|
1184
|
+
async collectTrajectoryFiles(dir) {
|
|
1185
|
+
const files = [];
|
|
1186
|
+
await this.walkJsonFilesInto(dir, files);
|
|
1187
|
+
return files;
|
|
1188
|
+
}
|
|
1189
|
+
async findTrajectoryFilePaths(id) {
|
|
1190
|
+
const pathsById = await this.findTrajectoryFilePathsForIds([id]);
|
|
1191
|
+
return pathsById.get(id) ?? [];
|
|
1192
|
+
}
|
|
1193
|
+
async findTrajectoryFilePathsForIds(ids) {
|
|
1194
|
+
const targetIds = new Set(Array.from(ids).filter(isSafeTrajectoryId));
|
|
1195
|
+
const pathsById = new Map(
|
|
1196
|
+
Array.from(targetIds).map((id) => [id, []])
|
|
1197
|
+
);
|
|
1198
|
+
if (targetIds.size === 0) {
|
|
1199
|
+
return pathsById;
|
|
1200
|
+
}
|
|
1201
|
+
const allFiles = await this.listTrajectoryFiles();
|
|
1202
|
+
for (const filePath of allFiles) {
|
|
1203
|
+
const trajectoryId = this.getTrajectoryIdFromPath(filePath);
|
|
1204
|
+
if (!trajectoryId || !targetIds.has(trajectoryId)) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
pathsById.get(trajectoryId)?.push(filePath);
|
|
1208
|
+
}
|
|
1209
|
+
return pathsById;
|
|
1210
|
+
}
|
|
1211
|
+
getTrajectoryIdFromPath(filePath) {
|
|
1212
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1213
|
+
const id = basename(dirname(filePath));
|
|
1214
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1215
|
+
}
|
|
1216
|
+
const name = basename(filePath);
|
|
1217
|
+
if (name.endsWith(".json")) {
|
|
1218
|
+
const id = name.slice(0, -".json".length);
|
|
1219
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1220
|
+
}
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
async removeTrajectoryFiles(paths, exceptPath) {
|
|
1224
|
+
const summary = this.emptyDeleteSummary();
|
|
1225
|
+
for (const filePath of paths) {
|
|
1226
|
+
if (filePath === exceptPath) {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
async removeTrajectoryFile(filePath, summary) {
|
|
1233
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1234
|
+
const trajectoryDir = dirname(filePath);
|
|
1235
|
+
await this.countDirectoryTrajectoryFiles(trajectoryDir, summary);
|
|
1236
|
+
await this.removeFileIfExists(
|
|
1237
|
+
join(dirname(trajectoryDir), `${basename(trajectoryDir)}.trace.json`),
|
|
1238
|
+
"trace",
|
|
1239
|
+
summary
|
|
1240
|
+
);
|
|
1241
|
+
await rm(trajectoryDir, { recursive: true, force: true });
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
await this.removeFileIfExists(filePath, "json", summary);
|
|
1245
|
+
await this.removeFileIfExists(
|
|
1246
|
+
getMarkdownOutputPath(filePath),
|
|
1247
|
+
"markdown",
|
|
1248
|
+
summary
|
|
1249
|
+
);
|
|
1250
|
+
await this.removeFileIfExists(
|
|
1251
|
+
getTraceOutputPath(filePath),
|
|
1252
|
+
"trace",
|
|
1253
|
+
summary
|
|
1254
|
+
);
|
|
1255
|
+
await this.removeFileIfExists(
|
|
1256
|
+
getLegacyCompactionMarkerPath(filePath),
|
|
1257
|
+
"compaction",
|
|
1258
|
+
summary
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
async countDirectoryTrajectoryFiles(trajectoryDir, summary) {
|
|
1262
|
+
await this.countFileIfExists(
|
|
1263
|
+
join(trajectoryDir, TRAJECTORY_FILE),
|
|
1264
|
+
"json",
|
|
1265
|
+
summary
|
|
1266
|
+
);
|
|
1267
|
+
await this.countFileIfExists(
|
|
1268
|
+
join(trajectoryDir, SUMMARY_FILE),
|
|
1269
|
+
"markdown",
|
|
1270
|
+
summary
|
|
1271
|
+
);
|
|
1272
|
+
await this.countFileIfExists(
|
|
1273
|
+
join(trajectoryDir, `${basename(trajectoryDir)}.trace.json`),
|
|
1274
|
+
"trace",
|
|
1275
|
+
summary
|
|
1276
|
+
);
|
|
1277
|
+
await this.countFileIfExists(
|
|
1278
|
+
join(trajectoryDir, "trace.json"),
|
|
1279
|
+
"trace",
|
|
1280
|
+
summary
|
|
1281
|
+
);
|
|
1282
|
+
await this.countFileIfExists(
|
|
1283
|
+
join(trajectoryDir, COMPACTION_FILE),
|
|
1284
|
+
"compaction",
|
|
1285
|
+
summary
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
async removeFileIfExists(path, kind, summary) {
|
|
1289
|
+
if (!existsSync(path)) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
await rm(path, { force: true });
|
|
1293
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1294
|
+
}
|
|
1295
|
+
async countFileIfExists(path, kind, summary) {
|
|
1296
|
+
if (existsSync(path)) {
|
|
1297
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
incrementDeleteSummary(kind, summary) {
|
|
1301
|
+
if (kind === "json") {
|
|
1302
|
+
summary.deletedJsonFiles += 1;
|
|
1303
|
+
summary.removedTrajectories += 1;
|
|
1304
|
+
} else if (kind === "markdown") {
|
|
1305
|
+
summary.deletedMarkdownFiles += 1;
|
|
1306
|
+
} else if (kind === "trace") {
|
|
1307
|
+
summary.deletedTraceFiles += 1;
|
|
1308
|
+
} else {
|
|
1309
|
+
summary.deletedCompactionFiles += 1;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
emptyDeleteSummary() {
|
|
1313
|
+
return {
|
|
1314
|
+
removedTrajectories: 0,
|
|
1315
|
+
deletedJsonFiles: 0,
|
|
1316
|
+
deletedMarkdownFiles: 0,
|
|
1317
|
+
deletedTraceFiles: 0,
|
|
1318
|
+
deletedCompactionFiles: 0
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
async listCompactionMarkerFiles() {
|
|
1322
|
+
const markerPaths = [];
|
|
1323
|
+
await this.walkFilesInto(
|
|
1324
|
+
this.activeDir,
|
|
1325
|
+
markerPaths,
|
|
1326
|
+
isCompactionMarkerFile
|
|
1327
|
+
);
|
|
1328
|
+
await this.walkFilesInto(
|
|
1329
|
+
this.completedDir,
|
|
1330
|
+
markerPaths,
|
|
1331
|
+
isCompactionMarkerFile
|
|
1332
|
+
);
|
|
1333
|
+
return markerPaths;
|
|
1334
|
+
}
|
|
1335
|
+
getCompactionMarkerPath(filePath, id) {
|
|
1336
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1337
|
+
return join(dirname(filePath), COMPACTION_FILE);
|
|
1338
|
+
}
|
|
1339
|
+
return join(dirname(filePath), `${id}${LEGACY_COMPACTION_SUFFIX}`);
|
|
1340
|
+
}
|
|
1341
|
+
getTrajectoryIdFromCompactionMarkerPath(markerPath) {
|
|
1342
|
+
if (basename(markerPath) === COMPACTION_FILE) {
|
|
1343
|
+
const id = basename(dirname(markerPath));
|
|
1344
|
+
return id.startsWith("traj_") ? id : void 0;
|
|
1345
|
+
}
|
|
1346
|
+
const markerName = basename(markerPath);
|
|
1347
|
+
return markerName.endsWith(LEGACY_COMPACTION_SUFFIX) ? markerName.slice(0, -LEGACY_COMPACTION_SUFFIX.length) : void 0;
|
|
1348
|
+
}
|
|
1349
|
+
async migrateLegacyIndexCompactionMarkers() {
|
|
1350
|
+
const indexPath = join(this.trajectoriesDir, "index.json");
|
|
1351
|
+
if (!existsSync(indexPath)) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
let parsed;
|
|
1355
|
+
try {
|
|
1356
|
+
parsed = JSON.parse(await readFile(indexPath, "utf-8"));
|
|
1357
|
+
} catch {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const trajectories = parsed.trajectories;
|
|
1364
|
+
if (trajectories === null || typeof trajectories !== "object" || Array.isArray(trajectories)) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
await Promise.all(
|
|
1368
|
+
Object.entries(trajectories).map(async ([id, entry]) => {
|
|
1369
|
+
if (entry === null || typeof entry !== "object" || !isSafeTrajectoryId(id)) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const compactedInto = entry.compactedInto;
|
|
1373
|
+
const path = entry.path;
|
|
1374
|
+
if (typeof compactedInto !== "string") {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const paths = typeof path === "string" && existsSync(path) && this.isPathInsideTrajectoriesDir(path) ? [path] : await this.findTrajectoryFilePaths(id);
|
|
1378
|
+
if (paths.length === 0) return;
|
|
1379
|
+
const marker = {
|
|
1380
|
+
trajectoryId: id,
|
|
1381
|
+
compactedInto,
|
|
1382
|
+
compactedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1383
|
+
};
|
|
1384
|
+
await Promise.all(
|
|
1385
|
+
paths.map(
|
|
1386
|
+
(filePath) => writeFile(
|
|
1387
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
1388
|
+
JSON.stringify(marker, null, 2),
|
|
1389
|
+
"utf-8"
|
|
1390
|
+
)
|
|
1391
|
+
)
|
|
1392
|
+
);
|
|
1393
|
+
})
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
isPathInsideTrajectoriesDir(path) {
|
|
1397
|
+
const rel = relative(resolve(this.trajectoriesDir), resolve(path));
|
|
1398
|
+
return Boolean(rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
1399
|
+
}
|
|
1400
|
+
async walkFilesInto(dir, out, predicate) {
|
|
1401
|
+
let entries;
|
|
1402
|
+
try {
|
|
1403
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (error.code === "ENOENT") return;
|
|
1406
|
+
throw error;
|
|
1407
|
+
}
|
|
1408
|
+
for (const entry of entries) {
|
|
1409
|
+
const entryPath = join(dir, entry.name);
|
|
1410
|
+
if (entry.isDirectory()) {
|
|
1411
|
+
await this.walkFilesInto(entryPath, out, predicate);
|
|
1412
|
+
} else if (entry.isFile() && predicate(entry.name)) {
|
|
1413
|
+
out.push(entryPath);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
getSortValue(trajectory2, sortBy) {
|
|
1418
|
+
if (sortBy === "title") {
|
|
1419
|
+
return trajectory2.task.title;
|
|
1420
|
+
}
|
|
1421
|
+
return trajectory2[sortBy] ?? "";
|
|
1422
|
+
}
|
|
1423
|
+
toSummary(trajectory2) {
|
|
1424
|
+
return {
|
|
1425
|
+
id: trajectory2.id,
|
|
1426
|
+
title: trajectory2.task.title,
|
|
1427
|
+
status: trajectory2.status,
|
|
1428
|
+
startedAt: trajectory2.startedAt,
|
|
1429
|
+
completedAt: trajectory2.completedAt,
|
|
1430
|
+
confidence: trajectory2.retrospective?.confidence,
|
|
1431
|
+
chapterCount: trajectory2.chapters.length,
|
|
1432
|
+
decisionCount: trajectory2.chapters.reduce(
|
|
1433
|
+
(count, chapter) => count + chapter.events.filter((event) => event.type === "decision").length,
|
|
1434
|
+
0
|
|
1435
|
+
)
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
isNewerTrajectory(candidate, current) {
|
|
1439
|
+
const candidateTime = new Date(
|
|
1440
|
+
candidate.completedAt ?? candidate.startedAt
|
|
1441
|
+
).getTime();
|
|
1442
|
+
const currentTime = new Date(
|
|
1443
|
+
current.completedAt ?? current.startedAt
|
|
1444
|
+
).getTime();
|
|
1445
|
+
return candidateTime > currentTime;
|
|
1446
|
+
}
|
|
1138
1447
|
/**
|
|
1139
1448
|
* Read a trajectory file and return a tagged result so callers can
|
|
1140
1449
|
* distinguish missing files, malformed JSON, and schema violations.
|
|
@@ -1174,82 +1483,25 @@ var FileStorage = class {
|
|
|
1174
1483
|
const result = await this.readTrajectoryFile(path);
|
|
1175
1484
|
return result.ok ? result.trajectory : null;
|
|
1176
1485
|
}
|
|
1177
|
-
/**
|
|
1178
|
-
* Read and parse the on-disk index.
|
|
1179
|
-
*
|
|
1180
|
-
* Tolerances (belt-and-braces against the read/write race):
|
|
1181
|
-
* - ENOENT: first-run, return an empty index silently.
|
|
1182
|
-
* - Empty file: a concurrent writer truncated index.json in "w" mode
|
|
1183
|
-
* right before we read. Return an empty index silently — this is
|
|
1184
|
-
* not a real corruption, just an interleaving the mutex + atomic
|
|
1185
|
-
* rename should already prevent. Logging here would be noise.
|
|
1186
|
-
* - Non-empty but malformed JSON: genuinely corrupted on disk (hand
|
|
1187
|
-
* edit, disk error, etc). Log it and return an empty index so the
|
|
1188
|
-
* caller can recover, but keep the log so the problem is visible.
|
|
1189
|
-
*/
|
|
1190
|
-
async loadIndex() {
|
|
1191
|
-
let content;
|
|
1192
|
-
try {
|
|
1193
|
-
content = await readFile(this.indexPath, "utf-8");
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
if (error.code !== "ENOENT") {
|
|
1196
|
-
console.error(
|
|
1197
|
-
"Error loading trajectory index, using empty index:",
|
|
1198
|
-
error
|
|
1199
|
-
);
|
|
1200
|
-
}
|
|
1201
|
-
return this.emptyIndex();
|
|
1202
|
-
}
|
|
1203
|
-
if (content.length === 0) {
|
|
1204
|
-
return this.emptyIndex();
|
|
1205
|
-
}
|
|
1206
|
-
try {
|
|
1207
|
-
return JSON.parse(content);
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
console.error(
|
|
1210
|
-
"Error loading trajectory index, using empty index:",
|
|
1211
|
-
error
|
|
1212
|
-
);
|
|
1213
|
-
return this.emptyIndex();
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
emptyIndex() {
|
|
1217
|
-
return {
|
|
1218
|
-
version: 1,
|
|
1219
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1220
|
-
trajectories: {}
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Atomic write: stage into a process-unique temp path in the same directory
|
|
1225
|
-
* and then rename over the live file. `rename` is atomic on POSIX, so
|
|
1226
|
-
* concurrent readers in any process either see the old complete file or
|
|
1227
|
-
* the new complete file — never a half-written / zero-byte state.
|
|
1228
|
-
*
|
|
1229
|
-
* Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
|
|
1230
|
-
* read-modify-write cycle stays serialized; the unique temp name also keeps
|
|
1231
|
-
* parallel writers in other processes from colliding on a shared tmp path.
|
|
1232
|
-
*/
|
|
1233
|
-
async saveIndex(index) {
|
|
1234
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1235
|
-
const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1236
|
-
await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
|
|
1237
|
-
await rename(tmpPath, this.indexPath);
|
|
1238
|
-
}
|
|
1239
|
-
async updateIndex(trajectory2, filePath) {
|
|
1240
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1241
|
-
const index = await this.loadIndex();
|
|
1242
|
-
index.trajectories[trajectory2.id] = {
|
|
1243
|
-
title: trajectory2.task.title,
|
|
1244
|
-
status: trajectory2.status,
|
|
1245
|
-
startedAt: trajectory2.startedAt,
|
|
1246
|
-
completedAt: trajectory2.completedAt,
|
|
1247
|
-
path: filePath
|
|
1248
|
-
};
|
|
1249
|
-
await this.saveIndex(index);
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
1486
|
};
|
|
1487
|
+
function isTrajectoryJsonFile(name) {
|
|
1488
|
+
return name === TRAJECTORY_FILE || name.endsWith(".json") && name !== "index.json" && !name.endsWith(".trace.json") && !name.endsWith(LEGACY_COMPACTION_SUFFIX) && name !== COMPACTION_FILE;
|
|
1489
|
+
}
|
|
1490
|
+
function isSafeTrajectoryId(id) {
|
|
1491
|
+
return id.length > 0 && !id.includes("..") && !id.includes("/") && !id.includes("\\");
|
|
1492
|
+
}
|
|
1493
|
+
function isCompactionMarkerFile(name) {
|
|
1494
|
+
return name === COMPACTION_FILE || name.endsWith(LEGACY_COMPACTION_SUFFIX);
|
|
1495
|
+
}
|
|
1496
|
+
function getMarkdownOutputPath(outputPath) {
|
|
1497
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
1498
|
+
}
|
|
1499
|
+
function getTraceOutputPath(outputPath) {
|
|
1500
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
|
|
1501
|
+
}
|
|
1502
|
+
function getLegacyCompactionMarkerPath(outputPath) {
|
|
1503
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(LEGACY_COMPACTION_SUFFIX) : `${outputPath}${LEGACY_COMPACTION_SUFFIX}`;
|
|
1504
|
+
}
|
|
1253
1505
|
|
|
1254
1506
|
// src/sdk/client.ts
|
|
1255
1507
|
var require2 = createRequire(import.meta.url);
|
|
@@ -1337,7 +1589,7 @@ async function compactWorkflow(workflowId, options) {
|
|
|
1337
1589
|
if (options?.discardSources) {
|
|
1338
1590
|
args.push("--discard-sources");
|
|
1339
1591
|
}
|
|
1340
|
-
return new Promise((
|
|
1592
|
+
return new Promise((resolve2, reject) => {
|
|
1341
1593
|
const child = spawn(cli.command, args, {
|
|
1342
1594
|
cwd: options?.cwd,
|
|
1343
1595
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1365,7 +1617,7 @@ async function compactWorkflow(workflowId, options) {
|
|
|
1365
1617
|
}
|
|
1366
1618
|
try {
|
|
1367
1619
|
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
1368
|
-
|
|
1620
|
+
resolve2(parseCompactWorkflowOutput(stdout));
|
|
1369
1621
|
} catch (error) {
|
|
1370
1622
|
reject(
|
|
1371
1623
|
error instanceof Error ? error : new Error("compactWorkflow failed: unable to parse CLI output")
|
|
@@ -2177,6 +2429,9 @@ export {
|
|
|
2177
2429
|
exportToMarkdown,
|
|
2178
2430
|
exportToPRSummary,
|
|
2179
2431
|
exportToTimeline,
|
|
2432
|
+
DEFAULT_TRAJECTORY_DATA_DIR,
|
|
2433
|
+
LEGACY_TRAJECTORY_DATA_DIR,
|
|
2434
|
+
getDefaultTrajectoryDataDir,
|
|
2180
2435
|
FileStorage,
|
|
2181
2436
|
compactWorkflow,
|
|
2182
2437
|
TrajectorySession,
|
|
@@ -2190,4 +2445,4 @@ export {
|
|
|
2190
2445
|
getCommitsBetween,
|
|
2191
2446
|
getFilesChangedBetween
|
|
2192
2447
|
};
|
|
2193
|
-
//# sourceMappingURL=chunk-
|
|
2448
|
+
//# sourceMappingURL=chunk-ENWKFNUD.js.map
|