agent-trajectories 0.5.5 → 0.5.7
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 +7 -4
- package/dist/{chunk-2LMOXJE7.js → chunk-27AQPWHK.js} +142 -74
- package/dist/chunk-27AQPWHK.js.map +1 -0
- package/dist/cli/index.js +229 -75
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-DiSIfGXl.d.ts → index-C7XhwsoN.d.ts} +28 -1
- 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-2LMOXJE7.js.map +0 -1
package/README.md
CHANGED
|
@@ -132,11 +132,14 @@ trail compact --commits abc1234,def5678 # Trajectories matching specific commit
|
|
|
132
132
|
trail compact --pr 123 # Trajectories mentioning PR #123
|
|
133
133
|
trail compact --since 7d # Last 7 days
|
|
134
134
|
trail compact --all # Everything (including previously compacted)
|
|
135
|
+
trail compact --pr 123 --discard-sources # Delete source trajectories and update index after compaction
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
### Automatic Compaction (GitHub Action)
|
|
138
139
|
|
|
139
|
-
Add these steps to any workflow that runs on PR merge (e.g., your release or publish flow). Requires `ref: ${{ github.event.pull_request.base.ref }}` and `fetch-depth: 0` on checkout, plus `contents: write` permission
|
|
140
|
+
Add these steps to any workflow that runs on PR merge (e.g., your release or publish flow). Requires `ref: ${{ github.event.pull_request.base.ref }}` and `fetch-depth: 0` on checkout, plus `contents: write` permission.
|
|
141
|
+
|
|
142
|
+
Use `--discard-sources` when the compacted summary should replace the raw source trajectories. This removes the source JSON/Markdown/trace files and updates `.trajectories/index.json`, reducing future list/search noise.
|
|
140
143
|
|
|
141
144
|
```yaml
|
|
142
145
|
- name: Compact trajectories
|
|
@@ -144,13 +147,13 @@ Add these steps to any workflow that runs on PR merge (e.g., your release or pub
|
|
|
144
147
|
PR_COMMITS=$(git log ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} --format=%H | paste -sd, -)
|
|
145
148
|
OUTPUT=".trajectories/compacted/pr-${{ github.event.pull_request.number }}.json"
|
|
146
149
|
if [ -n "$PR_COMMITS" ]; then
|
|
147
|
-
npx agent-trajectories compact --commits "$PR_COMMITS" --output "$OUTPUT"
|
|
150
|
+
npx agent-trajectories compact --commits "$PR_COMMITS" --output "$OUTPUT" --discard-sources
|
|
148
151
|
else
|
|
149
|
-
npx agent-trajectories compact --pr ${{ github.event.pull_request.number }} --output "$OUTPUT"
|
|
152
|
+
npx agent-trajectories compact --pr ${{ github.event.pull_request.number }} --output "$OUTPUT" --discard-sources
|
|
150
153
|
fi
|
|
151
154
|
- name: Commit compacted trajectories
|
|
152
155
|
run: |
|
|
153
|
-
git add .trajectories/
|
|
156
|
+
git add .trajectories/ || true
|
|
154
157
|
git diff --cached --quiet || \
|
|
155
158
|
(git commit -m "chore: compact trajectories for PR #${{ github.event.pull_request.number }}" && git push)
|
|
156
159
|
```
|
|
@@ -666,8 +666,16 @@ function formatTime(isoString) {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
// src/storage/file.ts
|
|
669
|
+
import { randomUUID } from "crypto";
|
|
669
670
|
import { existsSync } from "fs";
|
|
670
|
-
import {
|
|
671
|
+
import {
|
|
672
|
+
mkdir,
|
|
673
|
+
readFile,
|
|
674
|
+
readdir,
|
|
675
|
+
rename,
|
|
676
|
+
unlink,
|
|
677
|
+
writeFile
|
|
678
|
+
} from "fs/promises";
|
|
671
679
|
import { join } from "path";
|
|
672
680
|
function expandPath(path) {
|
|
673
681
|
if (path.startsWith("~")) {
|
|
@@ -675,6 +683,16 @@ function expandPath(path) {
|
|
|
675
683
|
}
|
|
676
684
|
return path;
|
|
677
685
|
}
|
|
686
|
+
var indexLocks = /* @__PURE__ */ new Map();
|
|
687
|
+
function withIndexLock(path, task) {
|
|
688
|
+
const prev = indexLocks.get(path) ?? Promise.resolve();
|
|
689
|
+
const next = prev.then(task, task);
|
|
690
|
+
indexLocks.set(
|
|
691
|
+
path,
|
|
692
|
+
next.catch(() => void 0)
|
|
693
|
+
);
|
|
694
|
+
return next;
|
|
695
|
+
}
|
|
678
696
|
var FileStorage = class {
|
|
679
697
|
baseDir;
|
|
680
698
|
trajectoriesDir;
|
|
@@ -701,10 +719,10 @@ var FileStorage = class {
|
|
|
701
719
|
await mkdir(this.activeDir, { recursive: true });
|
|
702
720
|
await mkdir(this.completedDir, { recursive: true });
|
|
703
721
|
if (!existsSync(this.indexPath)) {
|
|
704
|
-
await this.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
722
|
+
await withIndexLock(this.indexPath, async () => {
|
|
723
|
+
if (!existsSync(this.indexPath)) {
|
|
724
|
+
await this.saveIndex(this.emptyIndex());
|
|
725
|
+
}
|
|
708
726
|
});
|
|
709
727
|
}
|
|
710
728
|
await this.reconcileIndex();
|
|
@@ -732,49 +750,51 @@ var FileStorage = class {
|
|
|
732
750
|
skippedSchemaViolation: 0,
|
|
733
751
|
skippedIoError: 0
|
|
734
752
|
};
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
753
|
+
await withIndexLock(this.indexPath, async () => {
|
|
754
|
+
const index = await this.loadIndex();
|
|
755
|
+
const before = Object.keys(index.trajectories).length;
|
|
756
|
+
const discovered = [];
|
|
757
|
+
try {
|
|
758
|
+
const activeFiles = await readdir(this.activeDir);
|
|
759
|
+
for (const file of activeFiles) {
|
|
760
|
+
if (!file.endsWith(".json")) continue;
|
|
761
|
+
discovered.push(join(this.activeDir, file));
|
|
762
|
+
}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
if (error.code !== "ENOENT") throw error;
|
|
743
765
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
summary.skippedIoError += 1;
|
|
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;
|
|
758
779
|
}
|
|
759
|
-
|
|
780
|
+
const trajectory2 = result.trajectory;
|
|
781
|
+
if (index.trajectories[trajectory2.id]) {
|
|
782
|
+
summary.alreadyIndexed += 1;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
index.trajectories[trajectory2.id] = {
|
|
786
|
+
title: trajectory2.task.title,
|
|
787
|
+
status: trajectory2.status,
|
|
788
|
+
startedAt: trajectory2.startedAt,
|
|
789
|
+
completedAt: trajectory2.completedAt,
|
|
790
|
+
path: filePath
|
|
791
|
+
};
|
|
792
|
+
summary.added += 1;
|
|
760
793
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
summary.alreadyIndexed += 1;
|
|
764
|
-
continue;
|
|
794
|
+
if (Object.keys(index.trajectories).length !== before) {
|
|
795
|
+
await this.saveIndex(index);
|
|
765
796
|
}
|
|
766
|
-
|
|
767
|
-
title: trajectory2.task.title,
|
|
768
|
-
status: trajectory2.status,
|
|
769
|
-
startedAt: trajectory2.startedAt,
|
|
770
|
-
completedAt: trajectory2.completedAt,
|
|
771
|
-
path: filePath
|
|
772
|
-
};
|
|
773
|
-
summary.added += 1;
|
|
774
|
-
}
|
|
775
|
-
if (Object.keys(index.trajectories).length !== before) {
|
|
776
|
-
await this.saveIndex(index);
|
|
777
|
-
}
|
|
797
|
+
});
|
|
778
798
|
const hadSkips = summary.skippedMalformedJson + summary.skippedSchemaViolation + summary.skippedIoError > 0;
|
|
779
799
|
if (summary.added > 0 || hadSkips) {
|
|
780
800
|
const parts = [`reconciled ${summary.added}/${summary.scanned}`];
|
|
@@ -977,17 +997,19 @@ var FileStorage = class {
|
|
|
977
997
|
if (existsSync(activePath)) {
|
|
978
998
|
await unlink(activePath);
|
|
979
999
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
+
}
|
|
987
1009
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1010
|
+
delete index.trajectories[id];
|
|
1011
|
+
await this.saveIndex(index);
|
|
1012
|
+
});
|
|
991
1013
|
}
|
|
992
1014
|
/**
|
|
993
1015
|
* Search trajectories by text
|
|
@@ -1065,10 +1087,23 @@ var FileStorage = class {
|
|
|
1065
1087
|
const result = await this.readTrajectoryFile(path);
|
|
1066
1088
|
return result.ok ? result.trajectory : null;
|
|
1067
1089
|
}
|
|
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
|
+
*/
|
|
1068
1103
|
async loadIndex() {
|
|
1104
|
+
let content;
|
|
1069
1105
|
try {
|
|
1070
|
-
|
|
1071
|
-
return JSON.parse(content);
|
|
1106
|
+
content = await readFile(this.indexPath, "utf-8");
|
|
1072
1107
|
} catch (error) {
|
|
1073
1108
|
if (error.code !== "ENOENT") {
|
|
1074
1109
|
console.error(
|
|
@@ -1076,27 +1111,56 @@ var FileStorage = class {
|
|
|
1076
1111
|
error
|
|
1077
1112
|
);
|
|
1078
1113
|
}
|
|
1079
|
-
return
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
};
|
|
1114
|
+
return this.emptyIndex();
|
|
1115
|
+
}
|
|
1116
|
+
if (content.length === 0) {
|
|
1117
|
+
return this.emptyIndex();
|
|
1084
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
|
+
};
|
|
1085
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
|
+
*/
|
|
1086
1146
|
async saveIndex(index) {
|
|
1087
1147
|
index.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1088
|
-
|
|
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);
|
|
1089
1151
|
}
|
|
1090
1152
|
async updateIndex(trajectory2, filePath) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
+
});
|
|
1100
1164
|
}
|
|
1101
1165
|
};
|
|
1102
1166
|
|
|
@@ -1111,11 +1175,12 @@ function normalizeAutoCompactOptions(autoCompact) {
|
|
|
1111
1175
|
return false;
|
|
1112
1176
|
}
|
|
1113
1177
|
if (autoCompact === true) {
|
|
1114
|
-
return { mechanical: false, markdown: true };
|
|
1178
|
+
return { mechanical: false, markdown: true, discardSources: false };
|
|
1115
1179
|
}
|
|
1116
1180
|
return {
|
|
1117
1181
|
mechanical: autoCompact.mechanical ?? false,
|
|
1118
|
-
markdown: autoCompact.markdown ?? true
|
|
1182
|
+
markdown: autoCompact.markdown ?? true,
|
|
1183
|
+
discardSources: autoCompact.discardSources ?? false
|
|
1119
1184
|
};
|
|
1120
1185
|
}
|
|
1121
1186
|
function resolveStartWorkflowId(options) {
|
|
@@ -1182,6 +1247,9 @@ async function compactWorkflow(workflowId, options) {
|
|
|
1182
1247
|
if (options?.mechanical) {
|
|
1183
1248
|
args.push("--mechanical");
|
|
1184
1249
|
}
|
|
1250
|
+
if (options?.discardSources) {
|
|
1251
|
+
args.push("--discard-sources");
|
|
1252
|
+
}
|
|
1185
1253
|
return new Promise((resolve, reject) => {
|
|
1186
1254
|
const child = spawn(cli.command, args, {
|
|
1187
1255
|
cwd: options?.cwd,
|
|
@@ -2035,4 +2103,4 @@ export {
|
|
|
2035
2103
|
getCommitsBetween,
|
|
2036
2104
|
getFilesChangedBetween
|
|
2037
2105
|
};
|
|
2038
|
-
//# sourceMappingURL=chunk-
|
|
2106
|
+
//# sourceMappingURL=chunk-27AQPWHK.js.map
|