agent-trajectories 0.5.8 → 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-WMJRBQB4.js → chunk-JMH3Z5BB.js} +495 -265
- package/dist/chunk-JMH3Z5BB.js.map +1 -0
- package/dist/cli/index.js +553 -374
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-C9IcYSNQ.d.ts → index-B4yIThRL.d.ts} +59 -38
- 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-WMJRBQB4.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -404,17 +404,23 @@ function abandonTrajectory(trajectory, reason) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
// src/storage/file.ts
|
|
407
|
-
import { randomUUID } from "crypto";
|
|
408
407
|
import { existsSync } from "fs";
|
|
409
408
|
import {
|
|
410
409
|
mkdir,
|
|
411
410
|
readFile,
|
|
412
411
|
readdir,
|
|
413
412
|
rename,
|
|
414
|
-
|
|
413
|
+
rm,
|
|
415
414
|
writeFile
|
|
416
415
|
} from "fs/promises";
|
|
417
|
-
import {
|
|
416
|
+
import {
|
|
417
|
+
basename,
|
|
418
|
+
dirname,
|
|
419
|
+
isAbsolute,
|
|
420
|
+
join,
|
|
421
|
+
relative,
|
|
422
|
+
resolve
|
|
423
|
+
} from "path";
|
|
418
424
|
|
|
419
425
|
// src/export/markdown.ts
|
|
420
426
|
function exportToMarkdown(trajectory) {
|
|
@@ -577,6 +583,10 @@ function extractDecisions(trajectory) {
|
|
|
577
583
|
}
|
|
578
584
|
|
|
579
585
|
// src/storage/file.ts
|
|
586
|
+
var TRAJECTORY_FILE = "trajectory.json";
|
|
587
|
+
var SUMMARY_FILE = "summary.md";
|
|
588
|
+
var COMPACTION_FILE = "compaction.json";
|
|
589
|
+
var LEGACY_COMPACTION_SUFFIX = ".compaction.json";
|
|
580
590
|
function expandPath(path2) {
|
|
581
591
|
if (path2.startsWith("~")) {
|
|
582
592
|
return join(process.env.HOME ?? "", path2.slice(1));
|
|
@@ -608,22 +618,11 @@ function describeReadFailure(reason, error) {
|
|
|
608
618
|
if (error instanceof Error) return error.message;
|
|
609
619
|
return String(error);
|
|
610
620
|
}
|
|
611
|
-
var indexLocks = /* @__PURE__ */ new Map();
|
|
612
|
-
function withIndexLock(path2, task) {
|
|
613
|
-
const prev = indexLocks.get(path2) ?? Promise.resolve();
|
|
614
|
-
const next = prev.then(task, task);
|
|
615
|
-
indexLocks.set(
|
|
616
|
-
path2,
|
|
617
|
-
next.catch(() => void 0)
|
|
618
|
-
);
|
|
619
|
-
return next;
|
|
620
|
-
}
|
|
621
621
|
var FileStorage = class {
|
|
622
622
|
baseDir;
|
|
623
623
|
trajectoriesDir;
|
|
624
624
|
activeDir;
|
|
625
625
|
completedDir;
|
|
626
|
-
indexPath;
|
|
627
626
|
lastReconcileSummary;
|
|
628
627
|
constructor(baseDir) {
|
|
629
628
|
this.baseDir = baseDir ?? process.cwd();
|
|
@@ -635,7 +634,6 @@ var FileStorage = class {
|
|
|
635
634
|
}
|
|
636
635
|
this.activeDir = join(this.trajectoriesDir, "active");
|
|
637
636
|
this.completedDir = join(this.trajectoriesDir, "completed");
|
|
638
|
-
this.indexPath = join(this.trajectoriesDir, "index.json");
|
|
639
637
|
}
|
|
640
638
|
/**
|
|
641
639
|
* Initialize storage directories
|
|
@@ -644,28 +642,22 @@ var FileStorage = class {
|
|
|
644
642
|
await mkdir(this.trajectoriesDir, { recursive: true });
|
|
645
643
|
await mkdir(this.activeDir, { recursive: true });
|
|
646
644
|
await mkdir(this.completedDir, { recursive: true });
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (!existsSync(this.indexPath)) {
|
|
650
|
-
await this.saveIndex(this.emptyIndex());
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
}
|
|
645
|
+
await this.migrateLegacyIndexCompactionMarkers();
|
|
646
|
+
await rm(join(this.trajectoriesDir, "index.json"), { force: true });
|
|
654
647
|
await this.reconcileIndex();
|
|
655
648
|
}
|
|
656
649
|
/**
|
|
657
|
-
* Scan active/ and completed/ recursively and
|
|
658
|
-
*
|
|
659
|
-
* only adds, never removes.
|
|
650
|
+
* Scan active/ and completed/ recursively and report trajectory files
|
|
651
|
+
* that can be loaded plus files that should be surfaced by doctor.
|
|
660
652
|
*
|
|
661
653
|
* Handles three on-disk layouts in completed/:
|
|
662
654
|
* - flat: completed/{id}.json (legacy workforce data)
|
|
663
|
-
* - monthly: completed/YYYY-MM/{id}.json (
|
|
655
|
+
* - monthly: completed/YYYY-MM/{id}.json (legacy monthly layout)
|
|
656
|
+
* - directory: completed/YYYY-MM/{id}/trajectory.json (current layout)
|
|
664
657
|
* - nested: completed/.../{id}.json (defensive — any depth)
|
|
665
658
|
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* added.
|
|
659
|
+
* The method name is kept for callers such as `trail doctor`, but no
|
|
660
|
+
* shared index file is written.
|
|
669
661
|
*/
|
|
670
662
|
async reconcileIndex() {
|
|
671
663
|
const summary = {
|
|
@@ -677,58 +669,29 @@ var FileStorage = class {
|
|
|
677
669
|
skippedIoError: 0,
|
|
678
670
|
failures: []
|
|
679
671
|
};
|
|
680
|
-
await
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
if (error.code !== "ENOENT") throw error;
|
|
692
|
-
}
|
|
693
|
-
await this.walkJsonFilesInto(this.completedDir, discovered);
|
|
694
|
-
for (const filePath of discovered) {
|
|
695
|
-
summary.scanned += 1;
|
|
696
|
-
const result = await this.readTrajectoryFile(filePath);
|
|
697
|
-
if (!result.ok) {
|
|
698
|
-
if (result.reason === "malformed_json") {
|
|
699
|
-
summary.skippedMalformedJson += 1;
|
|
700
|
-
} else if (result.reason === "schema_violation") {
|
|
701
|
-
summary.skippedSchemaViolation += 1;
|
|
702
|
-
} else {
|
|
703
|
-
summary.skippedIoError += 1;
|
|
704
|
-
}
|
|
705
|
-
summary.failures.push({
|
|
706
|
-
path: result.path,
|
|
707
|
-
reason: result.reason,
|
|
708
|
-
message: describeReadFailure(result.reason, result.error)
|
|
709
|
-
});
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
const trajectory = result.trajectory;
|
|
713
|
-
if (index.trajectories[trajectory.id]) {
|
|
714
|
-
summary.alreadyIndexed += 1;
|
|
715
|
-
continue;
|
|
672
|
+
const discovered = await this.listTrajectoryFiles();
|
|
673
|
+
for (const filePath of discovered) {
|
|
674
|
+
summary.scanned += 1;
|
|
675
|
+
const result = await this.readTrajectoryFile(filePath);
|
|
676
|
+
if (!result.ok) {
|
|
677
|
+
if (result.reason === "malformed_json") {
|
|
678
|
+
summary.skippedMalformedJson += 1;
|
|
679
|
+
} else if (result.reason === "schema_violation") {
|
|
680
|
+
summary.skippedSchemaViolation += 1;
|
|
681
|
+
} else {
|
|
682
|
+
summary.skippedIoError += 1;
|
|
716
683
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
};
|
|
724
|
-
summary.added += 1;
|
|
725
|
-
}
|
|
726
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
727
|
-
await this.saveIndex(index);
|
|
684
|
+
summary.failures.push({
|
|
685
|
+
path: result.path,
|
|
686
|
+
reason: result.reason,
|
|
687
|
+
message: describeReadFailure(result.reason, result.error)
|
|
688
|
+
});
|
|
689
|
+
continue;
|
|
728
690
|
}
|
|
729
|
-
|
|
691
|
+
summary.added += 1;
|
|
692
|
+
}
|
|
730
693
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
731
|
-
if (
|
|
694
|
+
if (hadSkips) {
|
|
732
695
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
733
696
|
if (summary.skippedMalformedJson > 0) {
|
|
734
697
|
parts.push(`malformed: ${summary.skippedMalformedJson}`);
|
|
@@ -810,7 +773,7 @@ var FileStorage = class {
|
|
|
810
773
|
return dest;
|
|
811
774
|
}
|
|
812
775
|
/**
|
|
813
|
-
* Recursively collect
|
|
776
|
+
* Recursively collect trajectory JSON file paths under `dir` into `out`.
|
|
814
777
|
* Silently treats a missing directory as empty.
|
|
815
778
|
*/
|
|
816
779
|
async walkJsonFilesInto(dir, out) {
|
|
@@ -825,7 +788,7 @@ var FileStorage = class {
|
|
|
825
788
|
const entryPath = join(dir, entry.name);
|
|
826
789
|
if (entry.isDirectory()) {
|
|
827
790
|
await this.walkJsonFilesInto(entryPath, out);
|
|
828
|
-
} else if (entry.isFile() && entry.name
|
|
791
|
+
} else if (entry.isFile() && isTrajectoryJsonFile(entry.name)) {
|
|
829
792
|
out.push(entryPath);
|
|
830
793
|
}
|
|
831
794
|
}
|
|
@@ -849,56 +812,43 @@ var FileStorage = class {
|
|
|
849
812
|
}
|
|
850
813
|
const trajectory = validation.data;
|
|
851
814
|
const isCompleted = trajectory.status === "completed" || trajectory.status === "abandoned";
|
|
852
|
-
|
|
815
|
+
const existingPaths = await this.findTrajectoryFilePaths(trajectory.id);
|
|
816
|
+
let trajectoryDir;
|
|
853
817
|
if (isCompleted) {
|
|
854
818
|
const date = new Date(trajectory.completedAt ?? trajectory.startedAt);
|
|
855
819
|
const monthDir = join(
|
|
856
820
|
this.completedDir,
|
|
857
821
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
|
|
858
822
|
);
|
|
859
|
-
|
|
860
|
-
filePath = join(monthDir, `${trajectory.id}.json`);
|
|
861
|
-
const activePath = join(this.activeDir, `${trajectory.id}.json`);
|
|
862
|
-
if (existsSync(activePath)) {
|
|
863
|
-
await unlink(activePath);
|
|
864
|
-
}
|
|
865
|
-
const mdPath = join(monthDir, `${trajectory.id}.md`);
|
|
866
|
-
const markdown = exportToMarkdown(trajectory);
|
|
867
|
-
await writeFile(mdPath, markdown, "utf-8");
|
|
823
|
+
trajectoryDir = join(monthDir, trajectory.id);
|
|
868
824
|
} else {
|
|
869
|
-
|
|
825
|
+
trajectoryDir = join(this.activeDir, trajectory.id);
|
|
826
|
+
}
|
|
827
|
+
const filePath = join(trajectoryDir, TRAJECTORY_FILE);
|
|
828
|
+
await this.removeTrajectoryFiles(existingPaths, filePath);
|
|
829
|
+
await mkdir(trajectoryDir, { recursive: true });
|
|
830
|
+
if (isCompleted) {
|
|
831
|
+
const markdown = exportToMarkdown(trajectory);
|
|
832
|
+
await writeFile(join(trajectoryDir, SUMMARY_FILE), markdown, "utf-8");
|
|
870
833
|
}
|
|
871
834
|
await writeFile(filePath, JSON.stringify(trajectory, null, 2), "utf-8");
|
|
872
|
-
await this.updateIndex(trajectory, filePath);
|
|
873
835
|
}
|
|
874
836
|
/**
|
|
875
837
|
* Get a trajectory by ID
|
|
876
838
|
*/
|
|
877
839
|
async get(id) {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const entry = index.trajectories[id];
|
|
884
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
885
|
-
return this.readTrajectoryOrNull(entry.path);
|
|
886
|
-
}
|
|
887
|
-
try {
|
|
888
|
-
const flatPath = join(this.completedDir, `${id}.json`);
|
|
889
|
-
if (existsSync(flatPath)) {
|
|
890
|
-
return this.readTrajectoryOrNull(flatPath);
|
|
891
|
-
}
|
|
892
|
-
const months = await readdir(this.completedDir);
|
|
893
|
-
for (const month of months) {
|
|
894
|
-
const filePath = join(this.completedDir, month, `${id}.json`);
|
|
895
|
-
if (existsSync(filePath)) {
|
|
896
|
-
return this.readTrajectoryOrNull(filePath);
|
|
897
|
-
}
|
|
840
|
+
for (const filePath of this.getActiveCandidatePaths(id)) {
|
|
841
|
+
if (!existsSync(filePath)) continue;
|
|
842
|
+
const trajectory = await this.readTrajectoryOrNull(filePath);
|
|
843
|
+
if (trajectory?.id === id) {
|
|
844
|
+
return trajectory;
|
|
898
845
|
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
846
|
+
}
|
|
847
|
+
const paths = await this.findTrajectoryFilePaths(id);
|
|
848
|
+
for (const filePath of paths) {
|
|
849
|
+
const trajectory = await this.readTrajectoryOrNull(filePath);
|
|
850
|
+
if (trajectory?.id === id) {
|
|
851
|
+
return trajectory;
|
|
902
852
|
}
|
|
903
853
|
}
|
|
904
854
|
return null;
|
|
@@ -907,107 +857,61 @@ var FileStorage = class {
|
|
|
907
857
|
* Get the currently active trajectory
|
|
908
858
|
*/
|
|
909
859
|
async getActive() {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
913
|
-
if (jsonFiles.length === 0) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
let mostRecent = null;
|
|
917
|
-
let mostRecentTime = 0;
|
|
918
|
-
for (const file of jsonFiles) {
|
|
919
|
-
const trajectory = await this.readTrajectoryOrNull(
|
|
920
|
-
join(this.activeDir, file)
|
|
921
|
-
);
|
|
922
|
-
if (trajectory) {
|
|
923
|
-
const startTime = new Date(trajectory.startedAt).getTime();
|
|
924
|
-
if (startTime > mostRecentTime) {
|
|
925
|
-
mostRecentTime = startTime;
|
|
926
|
-
mostRecent = trajectory;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return mostRecent;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
if (error.code === "ENOENT") {
|
|
933
|
-
return null;
|
|
934
|
-
}
|
|
935
|
-
console.error("Error reading active trajectories:", error);
|
|
860
|
+
const activeFiles = await this.collectTrajectoryFiles(this.activeDir);
|
|
861
|
+
if (activeFiles.length === 0) {
|
|
936
862
|
return null;
|
|
937
863
|
}
|
|
864
|
+
let mostRecent = null;
|
|
865
|
+
let mostRecentTime = 0;
|
|
866
|
+
for (const filePath of activeFiles) {
|
|
867
|
+
const trajectory = await this.readTrajectoryOrNull(filePath);
|
|
868
|
+
if (trajectory?.status !== "active") continue;
|
|
869
|
+
const startTime = new Date(trajectory.startedAt).getTime();
|
|
870
|
+
if (startTime > mostRecentTime) {
|
|
871
|
+
mostRecentTime = startTime;
|
|
872
|
+
mostRecent = trajectory;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return mostRecent;
|
|
938
876
|
}
|
|
939
877
|
/**
|
|
940
878
|
* List trajectories with optional filtering
|
|
941
879
|
*/
|
|
942
880
|
async list(query) {
|
|
943
|
-
|
|
944
|
-
let entries = Object.entries(index.trajectories);
|
|
881
|
+
let trajectories = await this.loadAllTrajectories();
|
|
945
882
|
if (query.status) {
|
|
946
|
-
|
|
883
|
+
trajectories = trajectories.filter((t) => t.status === query.status);
|
|
947
884
|
}
|
|
948
885
|
if (query.since) {
|
|
949
886
|
const sinceTime = new Date(query.since).getTime();
|
|
950
|
-
|
|
951
|
-
(
|
|
887
|
+
trajectories = trajectories.filter(
|
|
888
|
+
(trajectory) => new Date(trajectory.startedAt).getTime() >= sinceTime
|
|
952
889
|
);
|
|
953
890
|
}
|
|
954
891
|
if (query.until) {
|
|
955
892
|
const untilTime = new Date(query.until).getTime();
|
|
956
|
-
|
|
957
|
-
(
|
|
893
|
+
trajectories = trajectories.filter(
|
|
894
|
+
(trajectory) => new Date(trajectory.startedAt).getTime() <= untilTime
|
|
958
895
|
);
|
|
959
896
|
}
|
|
960
897
|
const sortBy = query.sortBy ?? "startedAt";
|
|
961
898
|
const sortOrder = query.sortOrder ?? "desc";
|
|
962
|
-
|
|
963
|
-
const aVal = a
|
|
964
|
-
const bVal = b
|
|
899
|
+
trajectories.sort((a, b) => {
|
|
900
|
+
const aVal = this.getSortValue(a, sortBy);
|
|
901
|
+
const bVal = this.getSortValue(b, sortBy);
|
|
965
902
|
const cmp = String(aVal).localeCompare(String(bVal));
|
|
966
903
|
return sortOrder === "asc" ? cmp : -cmp;
|
|
967
904
|
});
|
|
968
905
|
const offset = query.offset ?? 0;
|
|
969
906
|
const limit = query.limit ?? 500;
|
|
970
|
-
|
|
971
|
-
return
|
|
972
|
-
entries.map(async ([id, entry]) => {
|
|
973
|
-
const trajectory = await this.get(id);
|
|
974
|
-
return {
|
|
975
|
-
id,
|
|
976
|
-
title: entry.title,
|
|
977
|
-
status: entry.status,
|
|
978
|
-
startedAt: entry.startedAt,
|
|
979
|
-
completedAt: entry.completedAt,
|
|
980
|
-
confidence: trajectory?.retrospective?.confidence,
|
|
981
|
-
chapterCount: trajectory?.chapters.length ?? 0,
|
|
982
|
-
decisionCount: trajectory?.chapters.reduce(
|
|
983
|
-
(count, chapter) => count + chapter.events.filter((e) => e.type === "decision").length,
|
|
984
|
-
0
|
|
985
|
-
) ?? 0
|
|
986
|
-
};
|
|
987
|
-
})
|
|
988
|
-
);
|
|
907
|
+
trajectories = trajectories.slice(offset, offset + limit);
|
|
908
|
+
return trajectories.map((trajectory) => this.toSummary(trajectory));
|
|
989
909
|
}
|
|
990
910
|
/**
|
|
991
911
|
* Delete a trajectory
|
|
992
912
|
*/
|
|
993
913
|
async delete(id) {
|
|
994
|
-
|
|
995
|
-
if (existsSync(activePath)) {
|
|
996
|
-
await unlink(activePath);
|
|
997
|
-
}
|
|
998
|
-
await withIndexLock(this.indexPath, async () => {
|
|
999
|
-
const index = await this.loadIndex();
|
|
1000
|
-
const entry = index.trajectories[id];
|
|
1001
|
-
if (entry?.path && existsSync(entry.path)) {
|
|
1002
|
-
await unlink(entry.path);
|
|
1003
|
-
const mdPath = entry.path.replace(".json", ".md");
|
|
1004
|
-
if (existsSync(mdPath)) {
|
|
1005
|
-
await unlink(mdPath);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
delete index.trajectories[id];
|
|
1009
|
-
await this.saveIndex(index);
|
|
1010
|
-
});
|
|
914
|
+
await this.deleteWithSummary(id);
|
|
1011
915
|
}
|
|
1012
916
|
/**
|
|
1013
917
|
* Search trajectories by text
|
|
@@ -1040,12 +944,395 @@ var FileStorage = class {
|
|
|
1040
944
|
}
|
|
1041
945
|
return matches;
|
|
1042
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Mark a trajectory as compacted without writing to a shared index.
|
|
949
|
+
*/
|
|
950
|
+
async markCompacted(id, compactedInto) {
|
|
951
|
+
const markedIds = await this.markCompactedMany([id], compactedInto);
|
|
952
|
+
return markedIds.has(id);
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Mark multiple trajectories as compacted with one filesystem scan.
|
|
956
|
+
*/
|
|
957
|
+
async markCompactedMany(ids, compactedInto) {
|
|
958
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
959
|
+
const markedIds = /* @__PURE__ */ new Set();
|
|
960
|
+
const compactedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
961
|
+
const writes = [];
|
|
962
|
+
for (const [id, paths] of pathsById.entries()) {
|
|
963
|
+
if (paths.length === 0) {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
markedIds.add(id);
|
|
967
|
+
const marker = {
|
|
968
|
+
trajectoryId: id,
|
|
969
|
+
compactedInto,
|
|
970
|
+
compactedAt
|
|
971
|
+
};
|
|
972
|
+
for (const filePath of paths) {
|
|
973
|
+
writes.push(
|
|
974
|
+
writeFile(
|
|
975
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
976
|
+
JSON.stringify(marker, null, 2),
|
|
977
|
+
"utf-8"
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
await Promise.all(writes);
|
|
983
|
+
return markedIds;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Return trajectory IDs that have a per-trajectory compaction marker.
|
|
987
|
+
*/
|
|
988
|
+
async getCompactedTrajectoryIds() {
|
|
989
|
+
const markerPaths = await this.listCompactionMarkerFiles();
|
|
990
|
+
const compactedIds = /* @__PURE__ */ new Set();
|
|
991
|
+
for (const markerPath of markerPaths) {
|
|
992
|
+
try {
|
|
993
|
+
const marker = JSON.parse(
|
|
994
|
+
await readFile(markerPath, "utf-8")
|
|
995
|
+
);
|
|
996
|
+
const trajectoryId = typeof marker.trajectoryId === "string" ? marker.trajectoryId : this.getTrajectoryIdFromCompactionMarkerPath(markerPath);
|
|
997
|
+
if (trajectoryId && typeof marker.compactedInto === "string") {
|
|
998
|
+
compactedIds.add(trajectoryId);
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return compactedIds;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Delete a trajectory and return file counts for CLI reporting.
|
|
1007
|
+
*/
|
|
1008
|
+
async deleteWithSummary(id) {
|
|
1009
|
+
return this.deleteManyWithSummary([id]);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Delete multiple trajectories with one filesystem scan.
|
|
1013
|
+
*/
|
|
1014
|
+
async deleteManyWithSummary(ids) {
|
|
1015
|
+
const summary = {
|
|
1016
|
+
removedTrajectories: 0,
|
|
1017
|
+
deletedJsonFiles: 0,
|
|
1018
|
+
deletedMarkdownFiles: 0,
|
|
1019
|
+
deletedTraceFiles: 0,
|
|
1020
|
+
deletedCompactionFiles: 0
|
|
1021
|
+
};
|
|
1022
|
+
const pathsById = await this.findTrajectoryFilePathsForIds(ids);
|
|
1023
|
+
const deletedPaths = /* @__PURE__ */ new Set();
|
|
1024
|
+
for (const paths of pathsById.values()) {
|
|
1025
|
+
for (const filePath of paths) {
|
|
1026
|
+
if (deletedPaths.has(filePath)) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
deletedPaths.add(filePath);
|
|
1030
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return summary;
|
|
1034
|
+
}
|
|
1043
1035
|
/**
|
|
1044
1036
|
* Close storage (no-op for file storage)
|
|
1045
1037
|
*/
|
|
1046
1038
|
async close() {
|
|
1047
1039
|
}
|
|
1048
1040
|
// Private helpers
|
|
1041
|
+
getActiveCandidatePaths(id) {
|
|
1042
|
+
if (!isSafeTrajectoryId(id)) {
|
|
1043
|
+
return [];
|
|
1044
|
+
}
|
|
1045
|
+
return [
|
|
1046
|
+
join(this.activeDir, id, TRAJECTORY_FILE),
|
|
1047
|
+
// Legacy layout from v0.5.x and earlier.
|
|
1048
|
+
join(this.activeDir, `${id}.json`)
|
|
1049
|
+
];
|
|
1050
|
+
}
|
|
1051
|
+
async loadAllTrajectories() {
|
|
1052
|
+
const files = await this.listTrajectoryFiles();
|
|
1053
|
+
const trajectories = /* @__PURE__ */ new Map();
|
|
1054
|
+
for (const filePath of files) {
|
|
1055
|
+
const trajectory = await this.readTrajectoryOrNull(filePath);
|
|
1056
|
+
if (!trajectory) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const current = trajectories.get(trajectory.id);
|
|
1060
|
+
if (!current || this.isNewerTrajectory(trajectory, current)) {
|
|
1061
|
+
trajectories.set(trajectory.id, trajectory);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return Array.from(trajectories.values());
|
|
1065
|
+
}
|
|
1066
|
+
async listTrajectoryFiles() {
|
|
1067
|
+
const [activeFiles, completedFiles] = await Promise.all([
|
|
1068
|
+
this.collectTrajectoryFiles(this.activeDir),
|
|
1069
|
+
this.collectTrajectoryFiles(this.completedDir)
|
|
1070
|
+
]);
|
|
1071
|
+
return [...activeFiles, ...completedFiles];
|
|
1072
|
+
}
|
|
1073
|
+
async collectTrajectoryFiles(dir) {
|
|
1074
|
+
const files = [];
|
|
1075
|
+
await this.walkJsonFilesInto(dir, files);
|
|
1076
|
+
return files;
|
|
1077
|
+
}
|
|
1078
|
+
async findTrajectoryFilePaths(id) {
|
|
1079
|
+
const pathsById = await this.findTrajectoryFilePathsForIds([id]);
|
|
1080
|
+
return pathsById.get(id) ?? [];
|
|
1081
|
+
}
|
|
1082
|
+
async findTrajectoryFilePathsForIds(ids) {
|
|
1083
|
+
const targetIds = new Set(Array.from(ids).filter(isSafeTrajectoryId));
|
|
1084
|
+
const pathsById = new Map(
|
|
1085
|
+
Array.from(targetIds).map((id) => [id, []])
|
|
1086
|
+
);
|
|
1087
|
+
if (targetIds.size === 0) {
|
|
1088
|
+
return pathsById;
|
|
1089
|
+
}
|
|
1090
|
+
const allFiles = await this.listTrajectoryFiles();
|
|
1091
|
+
for (const filePath of allFiles) {
|
|
1092
|
+
const trajectoryId = this.getTrajectoryIdFromPath(filePath);
|
|
1093
|
+
if (!trajectoryId || !targetIds.has(trajectoryId)) {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
pathsById.get(trajectoryId)?.push(filePath);
|
|
1097
|
+
}
|
|
1098
|
+
return pathsById;
|
|
1099
|
+
}
|
|
1100
|
+
getTrajectoryIdFromPath(filePath) {
|
|
1101
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1102
|
+
const id = basename(dirname(filePath));
|
|
1103
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1104
|
+
}
|
|
1105
|
+
const name = basename(filePath);
|
|
1106
|
+
if (name.endsWith(".json")) {
|
|
1107
|
+
const id = name.slice(0, -".json".length);
|
|
1108
|
+
return isSafeTrajectoryId(id) ? id : void 0;
|
|
1109
|
+
}
|
|
1110
|
+
return void 0;
|
|
1111
|
+
}
|
|
1112
|
+
async removeTrajectoryFiles(paths, exceptPath) {
|
|
1113
|
+
const summary = this.emptyDeleteSummary();
|
|
1114
|
+
for (const filePath of paths) {
|
|
1115
|
+
if (filePath === exceptPath) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
await this.removeTrajectoryFile(filePath, summary);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async removeTrajectoryFile(filePath, summary) {
|
|
1122
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1123
|
+
const trajectoryDir = dirname(filePath);
|
|
1124
|
+
await this.countDirectoryTrajectoryFiles(trajectoryDir, summary);
|
|
1125
|
+
await this.removeFileIfExists(
|
|
1126
|
+
join(dirname(trajectoryDir), `${basename(trajectoryDir)}.trace.json`),
|
|
1127
|
+
"trace",
|
|
1128
|
+
summary
|
|
1129
|
+
);
|
|
1130
|
+
await rm(trajectoryDir, { recursive: true, force: true });
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
await this.removeFileIfExists(filePath, "json", summary);
|
|
1134
|
+
await this.removeFileIfExists(
|
|
1135
|
+
getMarkdownOutputPath(filePath),
|
|
1136
|
+
"markdown",
|
|
1137
|
+
summary
|
|
1138
|
+
);
|
|
1139
|
+
await this.removeFileIfExists(
|
|
1140
|
+
getTraceOutputPath(filePath),
|
|
1141
|
+
"trace",
|
|
1142
|
+
summary
|
|
1143
|
+
);
|
|
1144
|
+
await this.removeFileIfExists(
|
|
1145
|
+
getLegacyCompactionMarkerPath(filePath),
|
|
1146
|
+
"compaction",
|
|
1147
|
+
summary
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
async countDirectoryTrajectoryFiles(trajectoryDir, summary) {
|
|
1151
|
+
await this.countFileIfExists(
|
|
1152
|
+
join(trajectoryDir, TRAJECTORY_FILE),
|
|
1153
|
+
"json",
|
|
1154
|
+
summary
|
|
1155
|
+
);
|
|
1156
|
+
await this.countFileIfExists(
|
|
1157
|
+
join(trajectoryDir, SUMMARY_FILE),
|
|
1158
|
+
"markdown",
|
|
1159
|
+
summary
|
|
1160
|
+
);
|
|
1161
|
+
await this.countFileIfExists(
|
|
1162
|
+
join(trajectoryDir, `${basename(trajectoryDir)}.trace.json`),
|
|
1163
|
+
"trace",
|
|
1164
|
+
summary
|
|
1165
|
+
);
|
|
1166
|
+
await this.countFileIfExists(
|
|
1167
|
+
join(trajectoryDir, "trace.json"),
|
|
1168
|
+
"trace",
|
|
1169
|
+
summary
|
|
1170
|
+
);
|
|
1171
|
+
await this.countFileIfExists(
|
|
1172
|
+
join(trajectoryDir, COMPACTION_FILE),
|
|
1173
|
+
"compaction",
|
|
1174
|
+
summary
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
async removeFileIfExists(path2, kind, summary) {
|
|
1178
|
+
if (!existsSync(path2)) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
await rm(path2, { force: true });
|
|
1182
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1183
|
+
}
|
|
1184
|
+
async countFileIfExists(path2, kind, summary) {
|
|
1185
|
+
if (existsSync(path2)) {
|
|
1186
|
+
this.incrementDeleteSummary(kind, summary);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
incrementDeleteSummary(kind, summary) {
|
|
1190
|
+
if (kind === "json") {
|
|
1191
|
+
summary.deletedJsonFiles += 1;
|
|
1192
|
+
summary.removedTrajectories += 1;
|
|
1193
|
+
} else if (kind === "markdown") {
|
|
1194
|
+
summary.deletedMarkdownFiles += 1;
|
|
1195
|
+
} else if (kind === "trace") {
|
|
1196
|
+
summary.deletedTraceFiles += 1;
|
|
1197
|
+
} else {
|
|
1198
|
+
summary.deletedCompactionFiles += 1;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
emptyDeleteSummary() {
|
|
1202
|
+
return {
|
|
1203
|
+
removedTrajectories: 0,
|
|
1204
|
+
deletedJsonFiles: 0,
|
|
1205
|
+
deletedMarkdownFiles: 0,
|
|
1206
|
+
deletedTraceFiles: 0,
|
|
1207
|
+
deletedCompactionFiles: 0
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
async listCompactionMarkerFiles() {
|
|
1211
|
+
const markerPaths = [];
|
|
1212
|
+
await this.walkFilesInto(
|
|
1213
|
+
this.activeDir,
|
|
1214
|
+
markerPaths,
|
|
1215
|
+
isCompactionMarkerFile
|
|
1216
|
+
);
|
|
1217
|
+
await this.walkFilesInto(
|
|
1218
|
+
this.completedDir,
|
|
1219
|
+
markerPaths,
|
|
1220
|
+
isCompactionMarkerFile
|
|
1221
|
+
);
|
|
1222
|
+
return markerPaths;
|
|
1223
|
+
}
|
|
1224
|
+
getCompactionMarkerPath(filePath, id) {
|
|
1225
|
+
if (basename(filePath) === TRAJECTORY_FILE) {
|
|
1226
|
+
return join(dirname(filePath), COMPACTION_FILE);
|
|
1227
|
+
}
|
|
1228
|
+
return join(dirname(filePath), `${id}${LEGACY_COMPACTION_SUFFIX}`);
|
|
1229
|
+
}
|
|
1230
|
+
getTrajectoryIdFromCompactionMarkerPath(markerPath) {
|
|
1231
|
+
if (basename(markerPath) === COMPACTION_FILE) {
|
|
1232
|
+
const id = basename(dirname(markerPath));
|
|
1233
|
+
return id.startsWith("traj_") ? id : void 0;
|
|
1234
|
+
}
|
|
1235
|
+
const markerName = basename(markerPath);
|
|
1236
|
+
return markerName.endsWith(LEGACY_COMPACTION_SUFFIX) ? markerName.slice(0, -LEGACY_COMPACTION_SUFFIX.length) : void 0;
|
|
1237
|
+
}
|
|
1238
|
+
async migrateLegacyIndexCompactionMarkers() {
|
|
1239
|
+
const indexPath = join(this.trajectoriesDir, "index.json");
|
|
1240
|
+
if (!existsSync(indexPath)) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
let parsed;
|
|
1244
|
+
try {
|
|
1245
|
+
parsed = JSON.parse(await readFile(indexPath, "utf-8"));
|
|
1246
|
+
} catch {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const trajectories = parsed.trajectories;
|
|
1253
|
+
if (trajectories === null || typeof trajectories !== "object" || Array.isArray(trajectories)) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
await Promise.all(
|
|
1257
|
+
Object.entries(trajectories).map(async ([id, entry]) => {
|
|
1258
|
+
if (entry === null || typeof entry !== "object" || !isSafeTrajectoryId(id)) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const compactedInto = entry.compactedInto;
|
|
1262
|
+
const path2 = entry.path;
|
|
1263
|
+
if (typeof compactedInto !== "string") {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const paths = typeof path2 === "string" && existsSync(path2) && this.isPathInsideTrajectoriesDir(path2) ? [path2] : await this.findTrajectoryFilePaths(id);
|
|
1267
|
+
if (paths.length === 0) return;
|
|
1268
|
+
const marker = {
|
|
1269
|
+
trajectoryId: id,
|
|
1270
|
+
compactedInto,
|
|
1271
|
+
compactedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1272
|
+
};
|
|
1273
|
+
await Promise.all(
|
|
1274
|
+
paths.map(
|
|
1275
|
+
(filePath) => writeFile(
|
|
1276
|
+
this.getCompactionMarkerPath(filePath, id),
|
|
1277
|
+
JSON.stringify(marker, null, 2),
|
|
1278
|
+
"utf-8"
|
|
1279
|
+
)
|
|
1280
|
+
)
|
|
1281
|
+
);
|
|
1282
|
+
})
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
isPathInsideTrajectoriesDir(path2) {
|
|
1286
|
+
const rel = relative(resolve(this.trajectoriesDir), resolve(path2));
|
|
1287
|
+
return Boolean(rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
1288
|
+
}
|
|
1289
|
+
async walkFilesInto(dir, out, predicate) {
|
|
1290
|
+
let entries;
|
|
1291
|
+
try {
|
|
1292
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
if (error.code === "ENOENT") return;
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1297
|
+
for (const entry of entries) {
|
|
1298
|
+
const entryPath = join(dir, entry.name);
|
|
1299
|
+
if (entry.isDirectory()) {
|
|
1300
|
+
await this.walkFilesInto(entryPath, out, predicate);
|
|
1301
|
+
} else if (entry.isFile() && predicate(entry.name)) {
|
|
1302
|
+
out.push(entryPath);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
getSortValue(trajectory, sortBy) {
|
|
1307
|
+
if (sortBy === "title") {
|
|
1308
|
+
return trajectory.task.title;
|
|
1309
|
+
}
|
|
1310
|
+
return trajectory[sortBy] ?? "";
|
|
1311
|
+
}
|
|
1312
|
+
toSummary(trajectory) {
|
|
1313
|
+
return {
|
|
1314
|
+
id: trajectory.id,
|
|
1315
|
+
title: trajectory.task.title,
|
|
1316
|
+
status: trajectory.status,
|
|
1317
|
+
startedAt: trajectory.startedAt,
|
|
1318
|
+
completedAt: trajectory.completedAt,
|
|
1319
|
+
confidence: trajectory.retrospective?.confidence,
|
|
1320
|
+
chapterCount: trajectory.chapters.length,
|
|
1321
|
+
decisionCount: trajectory.chapters.reduce(
|
|
1322
|
+
(count, chapter) => count + chapter.events.filter((event) => event.type === "decision").length,
|
|
1323
|
+
0
|
|
1324
|
+
)
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
isNewerTrajectory(candidate, current) {
|
|
1328
|
+
const candidateTime = new Date(
|
|
1329
|
+
candidate.completedAt ?? candidate.startedAt
|
|
1330
|
+
).getTime();
|
|
1331
|
+
const currentTime = new Date(
|
|
1332
|
+
current.completedAt ?? current.startedAt
|
|
1333
|
+
).getTime();
|
|
1334
|
+
return candidateTime > currentTime;
|
|
1335
|
+
}
|
|
1049
1336
|
/**
|
|
1050
1337
|
* Read a trajectory file and return a tagged result so callers can
|
|
1051
1338
|
* distinguish missing files, malformed JSON, and schema violations.
|
|
@@ -1085,82 +1372,25 @@ var FileStorage = class {
|
|
|
1085
1372
|
const result = await this.readTrajectoryFile(path2);
|
|
1086
1373
|
return result.ok ? result.trajectory : null;
|
|
1087
1374
|
}
|
|
1088
|
-
/**
|
|
1089
|
-
* Read and parse the on-disk index.
|
|
1090
|
-
*
|
|
1091
|
-
* Tolerances (belt-and-braces against the read/write race):
|
|
1092
|
-
* - ENOENT: first-run, return an empty index silently.
|
|
1093
|
-
* - Empty file: a concurrent writer truncated index.json in "w" mode
|
|
1094
|
-
* right before we read. Return an empty index silently — this is
|
|
1095
|
-
* not a real corruption, just an interleaving the mutex + atomic
|
|
1096
|
-
* rename should already prevent. Logging here would be noise.
|
|
1097
|
-
* - Non-empty but malformed JSON: genuinely corrupted on disk (hand
|
|
1098
|
-
* edit, disk error, etc). Log it and return an empty index so the
|
|
1099
|
-
* caller can recover, but keep the log so the problem is visible.
|
|
1100
|
-
*/
|
|
1101
|
-
async loadIndex() {
|
|
1102
|
-
let content;
|
|
1103
|
-
try {
|
|
1104
|
-
content = await readFile(this.indexPath, "utf-8");
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
if (error.code !== "ENOENT") {
|
|
1107
|
-
console.error(
|
|
1108
|
-
"Error loading trajectory index, using empty index:",
|
|
1109
|
-
error
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
return this.emptyIndex();
|
|
1113
|
-
}
|
|
1114
|
-
if (content.length === 0) {
|
|
1115
|
-
return this.emptyIndex();
|
|
1116
|
-
}
|
|
1117
|
-
try {
|
|
1118
|
-
return JSON.parse(content);
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
console.error(
|
|
1121
|
-
"Error loading trajectory index, using empty index:",
|
|
1122
|
-
error
|
|
1123
|
-
);
|
|
1124
|
-
return this.emptyIndex();
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
emptyIndex() {
|
|
1128
|
-
return {
|
|
1129
|
-
version: 1,
|
|
1130
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1131
|
-
trajectories: {}
|
|
1132
|
-
};
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Atomic write: stage into a process-unique temp path in the same directory
|
|
1136
|
-
* and then rename over the live file. `rename` is atomic on POSIX, so
|
|
1137
|
-
* concurrent readers in any process either see the old complete file or
|
|
1138
|
-
* the new complete file — never a half-written / zero-byte state.
|
|
1139
|
-
*
|
|
1140
|
-
* Callers MUST hold `withIndexLock(this.indexPath, ...)` so the in-process
|
|
1141
|
-
* read-modify-write cycle stays serialized; the unique temp name also keeps
|
|
1142
|
-
* parallel writers in other processes from colliding on a shared tmp path.
|
|
1143
|
-
*/
|
|
1144
|
-
async saveIndex(index) {
|
|
1145
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1146
|
-
const tmpPath = `${this.indexPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1147
|
-
await writeFile(tmpPath, JSON.stringify(index, null, 2), "utf-8");
|
|
1148
|
-
await rename(tmpPath, this.indexPath);
|
|
1149
|
-
}
|
|
1150
|
-
async updateIndex(trajectory, filePath) {
|
|
1151
|
-
await withIndexLock(this.indexPath, async () => {
|
|
1152
|
-
const index = await this.loadIndex();
|
|
1153
|
-
index.trajectories[trajectory.id] = {
|
|
1154
|
-
title: trajectory.task.title,
|
|
1155
|
-
status: trajectory.status,
|
|
1156
|
-
startedAt: trajectory.startedAt,
|
|
1157
|
-
completedAt: trajectory.completedAt,
|
|
1158
|
-
path: filePath
|
|
1159
|
-
};
|
|
1160
|
-
await this.saveIndex(index);
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
1375
|
};
|
|
1376
|
+
function isTrajectoryJsonFile(name) {
|
|
1377
|
+
return name === TRAJECTORY_FILE || name.endsWith(".json") && name !== "index.json" && !name.endsWith(".trace.json") && !name.endsWith(LEGACY_COMPACTION_SUFFIX) && name !== COMPACTION_FILE;
|
|
1378
|
+
}
|
|
1379
|
+
function isSafeTrajectoryId(id) {
|
|
1380
|
+
return id.length > 0 && !id.includes("..") && !id.includes("/") && !id.includes("\\");
|
|
1381
|
+
}
|
|
1382
|
+
function isCompactionMarkerFile(name) {
|
|
1383
|
+
return name === COMPACTION_FILE || name.endsWith(LEGACY_COMPACTION_SUFFIX);
|
|
1384
|
+
}
|
|
1385
|
+
function getMarkdownOutputPath(outputPath) {
|
|
1386
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
1387
|
+
}
|
|
1388
|
+
function getTraceOutputPath(outputPath) {
|
|
1389
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
|
|
1390
|
+
}
|
|
1391
|
+
function getLegacyCompactionMarkerPath(outputPath) {
|
|
1392
|
+
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(LEGACY_COMPACTION_SUFFIX) : `${outputPath}${LEGACY_COMPACTION_SUFFIX}`;
|
|
1393
|
+
}
|
|
1164
1394
|
|
|
1165
1395
|
// src/cli/commands/abandon.ts
|
|
1166
1396
|
function registerAbandonCommand(program2) {
|
|
@@ -1183,13 +1413,7 @@ function registerAbandonCommand(program2) {
|
|
|
1183
1413
|
|
|
1184
1414
|
// src/cli/commands/compact.ts
|
|
1185
1415
|
import { execFileSync } from "child_process";
|
|
1186
|
-
import {
|
|
1187
|
-
existsSync as existsSync3,
|
|
1188
|
-
mkdirSync,
|
|
1189
|
-
readFileSync as readFileSync2,
|
|
1190
|
-
unlinkSync,
|
|
1191
|
-
writeFileSync
|
|
1192
|
-
} from "fs";
|
|
1416
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
1193
1417
|
import { dirname as dirname2, join as join4 } from "path";
|
|
1194
1418
|
|
|
1195
1419
|
// src/compact/config.ts
|
|
@@ -1894,7 +2118,7 @@ function buildCliArgs(cli) {
|
|
|
1894
2118
|
}
|
|
1895
2119
|
}
|
|
1896
2120
|
function spawnWithStdin(command, args, input) {
|
|
1897
|
-
return new Promise((
|
|
2121
|
+
return new Promise((resolve2, reject) => {
|
|
1898
2122
|
const child = spawn(command, args, {
|
|
1899
2123
|
timeout: 3e5,
|
|
1900
2124
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1912,7 +2136,7 @@ function spawnWithStdin(command, args, input) {
|
|
|
1912
2136
|
new Error(`CLI exited with code ${code}: ${stderr.slice(0, 200)}`)
|
|
1913
2137
|
);
|
|
1914
2138
|
} else {
|
|
1915
|
-
|
|
2139
|
+
resolve2(Buffer.concat(chunks).toString().trim());
|
|
1916
2140
|
}
|
|
1917
2141
|
});
|
|
1918
2142
|
child.stdin.write(input);
|
|
@@ -2337,7 +2561,7 @@ function registerCompactCommand(program2) {
|
|
|
2337
2561
|
"Comma-separated focus areas to emphasize in LLM compaction"
|
|
2338
2562
|
).option("--markdown", "Also write a Markdown companion file").option("--no-markdown", "Skip writing a Markdown companion file").option(
|
|
2339
2563
|
"--discard-sources",
|
|
2340
|
-
"After saving the compaction, delete source trajectory JSON/MD/trace files
|
|
2564
|
+
"After saving the compaction, delete source trajectory JSON/MD/trace files"
|
|
2341
2565
|
).option("--dry-run", "Preview what would be compacted without saving").option("--output <path>", "Output path for compacted trajectory").action(async (options) => {
|
|
2342
2566
|
const trajectories = await loadTrajectories(options);
|
|
2343
2567
|
if (trajectories.length === 0) {
|
|
@@ -2378,7 +2602,7 @@ function registerCompactCommand(program2) {
|
|
|
2378
2602
|
markdownEnabled
|
|
2379
2603
|
);
|
|
2380
2604
|
if (options.discardSources) {
|
|
2381
|
-
const discardSummary = discardSourceTrajectories(trajectories);
|
|
2605
|
+
const discardSummary = await discardSourceTrajectories(trajectories);
|
|
2382
2606
|
printDiscardSummary(discardSummary);
|
|
2383
2607
|
} else {
|
|
2384
2608
|
await markTrajectoriesAsCompacted(
|
|
@@ -2390,7 +2614,7 @@ function registerCompactCommand(program2) {
|
|
|
2390
2614
|
Compacted trajectory saved to: ${outputPath2}`);
|
|
2391
2615
|
if (markdownEnabled) {
|
|
2392
2616
|
console.log(
|
|
2393
|
-
`Markdown summary saved to: ${
|
|
2617
|
+
`Markdown summary saved to: ${getMarkdownOutputPath2(outputPath2)}`
|
|
2394
2618
|
);
|
|
2395
2619
|
}
|
|
2396
2620
|
printCompactedSummary(mechanicalCompacted);
|
|
@@ -2439,7 +2663,7 @@ Compacted trajectory saved to: ${outputPath2}`);
|
|
|
2439
2663
|
const outputPath = options.output || getDefaultOutputPath(compacted, options.workflow);
|
|
2440
2664
|
saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
|
|
2441
2665
|
if (options.discardSources) {
|
|
2442
|
-
const discardSummary = discardSourceTrajectories(trajectories);
|
|
2666
|
+
const discardSummary = await discardSourceTrajectories(trajectories);
|
|
2443
2667
|
printDiscardSummary(discardSummary);
|
|
2444
2668
|
} else {
|
|
2445
2669
|
await markTrajectoriesAsCompacted(trajectories, compacted.id);
|
|
@@ -2448,7 +2672,7 @@ Compacted trajectory saved to: ${outputPath2}`);
|
|
|
2448
2672
|
Compacted trajectory saved to: ${outputPath}`);
|
|
2449
2673
|
if (markdownEnabled) {
|
|
2450
2674
|
console.log(
|
|
2451
|
-
`Markdown summary saved to: ${
|
|
2675
|
+
`Markdown summary saved to: ${getMarkdownOutputPath2(outputPath)}`
|
|
2452
2676
|
);
|
|
2453
2677
|
}
|
|
2454
2678
|
printCompactedSummary(compacted);
|
|
@@ -2467,19 +2691,12 @@ async function loadTrajectories(options) {
|
|
|
2467
2691
|
return [trimmed, trimmed.slice(0, 7)];
|
|
2468
2692
|
})
|
|
2469
2693
|
) : null;
|
|
2470
|
-
const compactedIds = options.all ? /* @__PURE__ */ new Set() : getCompactedTrajectoryIds();
|
|
2694
|
+
const compactedIds = options.all ? /* @__PURE__ */ new Set() : await getCompactedTrajectoryIds();
|
|
2471
2695
|
const searchPaths = getSearchPaths();
|
|
2472
2696
|
const seenIds = /* @__PURE__ */ new Set();
|
|
2473
2697
|
for (const searchPath of searchPaths) {
|
|
2474
2698
|
if (!existsSync3(searchPath)) continue;
|
|
2475
|
-
const
|
|
2476
|
-
process.env.TRAJECTORIES_DATA_DIR = searchPath;
|
|
2477
|
-
const storage = new FileStorage();
|
|
2478
|
-
if (originalDataDir !== void 0) {
|
|
2479
|
-
process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
|
|
2480
|
-
} else {
|
|
2481
|
-
delete process.env.TRAJECTORIES_DATA_DIR;
|
|
2482
|
-
}
|
|
2699
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2483
2700
|
await storage.initialize();
|
|
2484
2701
|
const summaries = await storage.list({
|
|
2485
2702
|
status: "completed",
|
|
@@ -2525,6 +2742,19 @@ async function loadTrajectories(options) {
|
|
|
2525
2742
|
}
|
|
2526
2743
|
return trajectories;
|
|
2527
2744
|
}
|
|
2745
|
+
function createStorageForSearchPath(searchPath) {
|
|
2746
|
+
const originalDataDir = process.env.TRAJECTORIES_DATA_DIR;
|
|
2747
|
+
process.env.TRAJECTORIES_DATA_DIR = searchPath;
|
|
2748
|
+
try {
|
|
2749
|
+
return new FileStorage();
|
|
2750
|
+
} finally {
|
|
2751
|
+
if (originalDataDir !== void 0) {
|
|
2752
|
+
process.env.TRAJECTORIES_DATA_DIR = originalDataDir;
|
|
2753
|
+
} else {
|
|
2754
|
+
delete process.env.TRAJECTORIES_DATA_DIR;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2528
2758
|
function getBranchCommits(targetBranch) {
|
|
2529
2759
|
const commits = /* @__PURE__ */ new Set();
|
|
2530
2760
|
try {
|
|
@@ -2549,21 +2779,17 @@ function getBranchCommits(targetBranch) {
|
|
|
2549
2779
|
}
|
|
2550
2780
|
return commits;
|
|
2551
2781
|
}
|
|
2552
|
-
function getCompactedTrajectoryIds() {
|
|
2782
|
+
async function getCompactedTrajectoryIds() {
|
|
2553
2783
|
const compacted = /* @__PURE__ */ new Set();
|
|
2554
2784
|
const searchPaths = getSearchPaths();
|
|
2555
2785
|
for (const searchPath of searchPaths) {
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
compacted.add(id);
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
} catch {
|
|
2786
|
+
if (!existsSync3(searchPath)) {
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2790
|
+
await storage.initialize();
|
|
2791
|
+
for (const id of await storage.getCompactedTrajectoryIds()) {
|
|
2792
|
+
compacted.add(id);
|
|
2567
2793
|
}
|
|
2568
2794
|
}
|
|
2569
2795
|
return compacted;
|
|
@@ -2571,92 +2797,45 @@ function getCompactedTrajectoryIds() {
|
|
|
2571
2797
|
async function markTrajectoriesAsCompacted(trajectories, compactedIntoId) {
|
|
2572
2798
|
const searchPaths = getSearchPaths();
|
|
2573
2799
|
for (const searchPath of searchPaths) {
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
try {
|
|
2577
|
-
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
2578
|
-
const index = JSON.parse(indexContent);
|
|
2579
|
-
let updated = false;
|
|
2580
|
-
for (const traj of trajectories) {
|
|
2581
|
-
if (index.trajectories[traj.id]) {
|
|
2582
|
-
index.trajectories[traj.id].compactedInto = compactedIntoId;
|
|
2583
|
-
updated = true;
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
if (updated) {
|
|
2587
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2588
|
-
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
2589
|
-
}
|
|
2590
|
-
} catch {
|
|
2800
|
+
if (!existsSync3(searchPath)) {
|
|
2801
|
+
continue;
|
|
2591
2802
|
}
|
|
2803
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2804
|
+
await storage.initialize();
|
|
2805
|
+
await storage.markCompactedMany(
|
|
2806
|
+
trajectories.map((trajectory) => trajectory.id),
|
|
2807
|
+
compactedIntoId
|
|
2808
|
+
);
|
|
2592
2809
|
}
|
|
2593
2810
|
}
|
|
2594
|
-
function discardSourceTrajectories(trajectories) {
|
|
2595
|
-
const sourceIds = new Set(trajectories.map((trajectory) => trajectory.id));
|
|
2811
|
+
async function discardSourceTrajectories(trajectories) {
|
|
2596
2812
|
const summary = {
|
|
2597
|
-
|
|
2813
|
+
removedTrajectories: 0,
|
|
2598
2814
|
deletedJsonFiles: 0,
|
|
2599
2815
|
deletedMarkdownFiles: 0,
|
|
2600
|
-
deletedTraceFiles: 0
|
|
2816
|
+
deletedTraceFiles: 0,
|
|
2817
|
+
deletedCompactionFiles: 0
|
|
2601
2818
|
};
|
|
2602
2819
|
for (const searchPath of getSearchPaths()) {
|
|
2603
|
-
|
|
2604
|
-
if (!existsSync3(indexPath)) continue;
|
|
2605
|
-
let index;
|
|
2606
|
-
try {
|
|
2607
|
-
const indexContent = readFileSync2(indexPath, "utf-8");
|
|
2608
|
-
const parsedIndex = JSON.parse(indexContent);
|
|
2609
|
-
if (!isTrajectoryIndex(parsedIndex)) {
|
|
2610
|
-
continue;
|
|
2611
|
-
}
|
|
2612
|
-
index = parsedIndex;
|
|
2613
|
-
} catch {
|
|
2820
|
+
if (!existsSync3(searchPath)) {
|
|
2614
2821
|
continue;
|
|
2615
2822
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
if (deleteFileIfExists(getTraceOutputPath(entry.path))) {
|
|
2627
|
-
summary.deletedTraceFiles += 1;
|
|
2628
|
-
}
|
|
2629
|
-
delete index.trajectories[id];
|
|
2630
|
-
summary.removedIndexEntries += 1;
|
|
2631
|
-
updated = true;
|
|
2632
|
-
}
|
|
2633
|
-
if (updated) {
|
|
2634
|
-
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
2635
|
-
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
2636
|
-
}
|
|
2823
|
+
const storage = createStorageForSearchPath(searchPath);
|
|
2824
|
+
await storage.initialize();
|
|
2825
|
+
const deleteSummary = await storage.deleteManyWithSummary(
|
|
2826
|
+
trajectories.map((trajectory) => trajectory.id)
|
|
2827
|
+
);
|
|
2828
|
+
summary.removedTrajectories += deleteSummary.removedTrajectories;
|
|
2829
|
+
summary.deletedJsonFiles += deleteSummary.deletedJsonFiles;
|
|
2830
|
+
summary.deletedMarkdownFiles += deleteSummary.deletedMarkdownFiles;
|
|
2831
|
+
summary.deletedTraceFiles += deleteSummary.deletedTraceFiles;
|
|
2832
|
+
summary.deletedCompactionFiles += deleteSummary.deletedCompactionFiles;
|
|
2637
2833
|
}
|
|
2638
2834
|
return summary;
|
|
2639
2835
|
}
|
|
2640
|
-
function deleteFileIfExists(path2) {
|
|
2641
|
-
if (!existsSync3(path2)) {
|
|
2642
|
-
return false;
|
|
2643
|
-
}
|
|
2644
|
-
unlinkSync(path2);
|
|
2645
|
-
return true;
|
|
2646
|
-
}
|
|
2647
|
-
function isTrajectoryIndex(value) {
|
|
2648
|
-
if (value === null || typeof value !== "object") {
|
|
2649
|
-
return false;
|
|
2650
|
-
}
|
|
2651
|
-
const candidate = value;
|
|
2652
|
-
return candidate.trajectories !== null && typeof candidate.trajectories === "object" && !Array.isArray(candidate.trajectories);
|
|
2653
|
-
}
|
|
2654
|
-
function getTraceOutputPath(outputPath) {
|
|
2655
|
-
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".trace.json") : `${outputPath}.trace.json`;
|
|
2656
|
-
}
|
|
2657
2836
|
function printDiscardSummary(summary) {
|
|
2658
2837
|
console.log(
|
|
2659
|
-
`Discarded source trajectories: ${summary.
|
|
2838
|
+
`Discarded source trajectories: ${summary.removedTrajectories} trajectories, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files, ${summary.deletedCompactionFiles} compaction markers`
|
|
2660
2839
|
);
|
|
2661
2840
|
}
|
|
2662
2841
|
function parseRelativeDate(input) {
|
|
@@ -2901,12 +3080,12 @@ function saveCompactionArtifacts(compacted, outputPath, markdownEnabled) {
|
|
|
2901
3080
|
writeFileSync(outputPath, JSON.stringify(compacted, null, 2));
|
|
2902
3081
|
if (markdownEnabled) {
|
|
2903
3082
|
writeFileSync(
|
|
2904
|
-
|
|
3083
|
+
getMarkdownOutputPath2(outputPath),
|
|
2905
3084
|
renderCompactionMarkdown(compacted)
|
|
2906
3085
|
);
|
|
2907
3086
|
}
|
|
2908
3087
|
}
|
|
2909
|
-
function
|
|
3088
|
+
function getMarkdownOutputPath2(outputPath) {
|
|
2910
3089
|
return outputPath.endsWith(".json") ? outputPath.slice(0, -".json".length).concat(".md") : `${outputPath}.md`;
|
|
2911
3090
|
}
|
|
2912
3091
|
function renderCompactionMarkdown(compacted) {
|
|
@@ -3229,7 +3408,7 @@ function createTraceRef(startRef, traceId) {
|
|
|
3229
3408
|
|
|
3230
3409
|
// src/core/trailers.ts
|
|
3231
3410
|
import { execSync as execSync2 } from "child_process";
|
|
3232
|
-
import { readFileSync as
|
|
3411
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3233
3412
|
function getCommitsBetween(startRef, endRef = "HEAD") {
|
|
3234
3413
|
if (!isGitRepo()) {
|
|
3235
3414
|
return [];
|
|
@@ -3342,7 +3521,7 @@ function detectExistingHook() {
|
|
|
3342
3521
|
}).trim();
|
|
3343
3522
|
const hookPath = `${hooksDir}/hooks/prepare-commit-msg`;
|
|
3344
3523
|
try {
|
|
3345
|
-
const content =
|
|
3524
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
3346
3525
|
if (content.includes("agent-trajectories")) {
|
|
3347
3526
|
return "ours";
|
|
3348
3527
|
}
|
|
@@ -3602,8 +3781,8 @@ function registerEnableCommand(program2) {
|
|
|
3602
3781
|
console.error("Remove it manually if needed");
|
|
3603
3782
|
throw new Error("Hook not ours");
|
|
3604
3783
|
}
|
|
3605
|
-
const { unlink
|
|
3606
|
-
await
|
|
3784
|
+
const { unlink } = await import("fs/promises");
|
|
3785
|
+
await unlink(hookPath);
|
|
3607
3786
|
console.log("Trajectory hook removed");
|
|
3608
3787
|
});
|
|
3609
3788
|
}
|