Skip to content

Commit e6ad1d1

Browse files
committed
Add compaction source pruning
1 parent 6ec9687 commit e6ad1d1

9 files changed

Lines changed: 282 additions & 39 deletions

File tree

AGENTS.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- prpm:snippet:start @agent-workforce/trail-snippet@1.1.0 -->
1+
<!-- prpm:snippet:start @agent-workforce/trail-snippet@1.1.2 -->
22
# Trail
33

44
Record your work as a trajectory for future agents and humans to follow.
@@ -82,6 +82,20 @@ When done, complete with a retrospective:
8282
trail complete --summary "Added JWT auth with refresh tokens" --confidence 0.85
8383
```
8484

85+
After completing work, compact the finished trajectory or merged PR into a
86+
durable summary. When the compacted summary is sufficient, discard the raw
87+
source trajectories so `.trajectories/index.json` and list output stay focused:
88+
89+
```bash
90+
trail compact --discard-sources
91+
# or after a PR merge:
92+
trail compact --pr 42 --discard-sources
93+
```
94+
95+
`--discard-sources` removes the source trajectory JSON/Markdown/trace files and
96+
updates the index. Use it after confirming the compacted artifact is the record
97+
you want to keep.
98+
8599
**Confidence levels:**
86100
- 0.9+ : High confidence, well-tested
87101
- 0.7-0.9 : Good confidence, standard implementation
@@ -122,23 +136,26 @@ trail export <trajectory-id> --format markdown
122136

123137
## Compacting Trajectories
124138

125-
After a PR merge, compact related trajectories into a single summary:
139+
After a PR merge, compact related trajectories into a single summary and prune
140+
raw source trajectories when the summary should replace them:
126141

127142
```bash
128-
trail compact --pr 42
143+
trail compact --pr 42 --discard-sources
129144
```
130145

131146
Compact by branch:
132147
```bash
133-
trail compact --branch feature/auth
148+
trail compact --branch feature/auth --discard-sources
134149
```
135150

136151
Compact by commit range:
137152
```bash
138-
trail compact --commits abc123..def456
153+
trail compact --commits abc123..def456 --discard-sources
139154
```
140155

141-
Compaction consolidates decisions and creates a grouped summary, reducing noise while preserving key decisions.
156+
Compaction consolidates decisions and creates a grouped summary. Adding
157+
`--discard-sources` makes the compacted artifact the durable record by removing
158+
the raw trajectories and their index entries.
142159

143160
## Why Trail?
144161

@@ -149,7 +166,7 @@ Your trajectory helps others understand:
149166
- **What challenges** you faced
150167

151168
Future agents can query past trajectories to learn from your decisions.
152-
<!-- prpm:snippet:end @agent-workforce/trail-snippet@1.1.0 -->
169+
<!-- prpm:snippet:end @agent-workforce/trail-snippet@1.1.2 -->
153170

154171
<!-- prpm:snippet:start @agent-relay/agent-relay-snippet@1.1.7 -->
155172
# Agent Relay

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,25 +132,28 @@ trail compact --commits abc1234,def5678 # Trajectories matching specific commit
132132
trail compact --pr 123 # Trajectories mentioning PR #123
133133
trail compact --since 7d # Last 7 days
134134
trail compact --all # Everything (including previously compacted)
135+
trail compact --pr 123 --discard-sources # Delete source trajectories and update index after compaction
135136
```
136137

137138
### Automatic Compaction (GitHub Action)
138139

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.
140143

141144
```yaml
142145
- name: Compact trajectories
143146
run: |
144147
PR_COMMITS=$(git log ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} --format=%H | paste -sd, -)
145148
OUTPUT=".trajectories/compacted/pr-${{ github.event.pull_request.number }}.json"
146149
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
148151
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
150153
fi
151154
- name: Commit compacted trajectories
152155
run: |
153-
git add .trajectories/compacted/ || true
156+
git add .trajectories/ || true
154157
git diff --cached --quiet || \
155158
(git commit -m "chore: compact trajectories for PR #${{ github.event.pull_request.number }}" && git push)
156159
```

docs/trail-snippet.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ When done, complete with a retrospective:
8181
trail complete --summary "Added JWT auth with refresh tokens" --confidence 0.85
8282
```
8383

84+
After completing work, compact the finished trajectory or merged PR into a
85+
durable summary. When the compacted summary is sufficient, discard the raw
86+
source trajectories so `.trajectories/index.json` and list output stay focused:
87+
88+
```bash
89+
trail compact --discard-sources
90+
# or after a PR merge:
91+
trail compact --pr 42 --discard-sources
92+
```
93+
94+
`--discard-sources` removes the source trajectory JSON/Markdown/trace files and
95+
updates the index. Use it after confirming the compacted artifact is the record
96+
you want to keep.
97+
8498
**Confidence levels:**
8599
- 0.9+ : High confidence, well-tested
86100
- 0.7-0.9 : Good confidence, standard implementation
@@ -121,23 +135,26 @@ trail export <trajectory-id> --format markdown
121135

122136
## Compacting Trajectories
123137

124-
After a PR merge, compact related trajectories into a single summary:
138+
After a PR merge, compact related trajectories into a single summary and prune
139+
raw source trajectories when the summary should replace them:
125140

126141
```bash
127-
trail compact --pr 42
142+
trail compact --pr 42 --discard-sources
128143
```
129144

130145
Compact by branch (finds trajectories with commits not in the specified base branch):
131146
```bash
132-
trail compact --branch main
147+
trail compact --branch main --discard-sources
133148
```
134149

135150
Compact by specific commits:
136151
```bash
137-
trail compact --commits abc123,def456
152+
trail compact --commits abc123,def456 --discard-sources
138153
```
139154

140-
Compaction consolidates decisions and creates a grouped summary, reducing noise while preserving key decisions.
155+
Compaction consolidates decisions and creates a grouped summary. Adding
156+
`--discard-sources` makes the compacted artifact the durable record by removing
157+
the raw trajectories and their index entries.
141158

142159
## Why Trail?
143160

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@
4848
"type": "git",
4949
"url": "https://github.com/AgentWorkforce/trajectories"
5050
},
51-
"files": [
52-
"dist"
53-
],
51+
"files": ["dist"],
5452
"engines": {
5553
"node": ">=20.0.0"
5654
},

prpm.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"packages": [
1010
{
1111
"name": "trail-snippet",
12-
"version": "1.1.1",
12+
"version": "1.1.2",
1313
"description": "AGENTS.md / CLAUDE.md snippet for agents on how to use trail to record their work",
1414
"format": "generic",
1515
"subtype": "snippet",

src/cli/commands/compact.ts

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
*/
1010

1111
import { execFileSync } from "node:child_process";
12-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12+
import {
13+
existsSync,
14+
mkdirSync,
15+
readFileSync,
16+
unlinkSync,
17+
writeFileSync,
18+
} from "node:fs";
1319
import { dirname, join } from "node:path";
1420
import type { Command } from "commander";
1521
import { getCompactionConfig } from "../../compact/config.js";
@@ -75,6 +81,12 @@ interface IndexEntry {
7581
compactedInto?: string;
7682
}
7783

84+
interface TrajectoryIndex {
85+
version: number;
86+
lastUpdated: string;
87+
trajectories: Record<string, IndexEntry>;
88+
}
89+
7890
interface CompactCommandOptions {
7991
since?: string;
8092
until?: string;
@@ -88,10 +100,18 @@ interface CompactCommandOptions {
88100
mechanical?: boolean;
89101
focus?: string;
90102
markdown?: boolean;
103+
discardSources?: boolean;
91104
dryRun?: boolean;
92105
output?: string;
93106
}
94107

108+
interface DiscardSourcesSummary {
109+
removedIndexEntries: number;
110+
deletedJsonFiles: number;
111+
deletedMarkdownFiles: number;
112+
deletedTraceFiles: number;
113+
}
114+
95115
interface LLMCompactionPlan {
96116
messages: Message[];
97117
estimatedInputTokens: number;
@@ -137,6 +157,10 @@ export function registerCompactCommand(program: Command): void {
137157
)
138158
.option("--markdown", "Also write a Markdown companion file")
139159
.option("--no-markdown", "Skip writing a Markdown companion file")
160+
.option(
161+
"--discard-sources",
162+
"After saving the compaction, delete source trajectory JSON/MD/trace files and remove their index entries",
163+
)
140164
.option("--dry-run", "Preview what would be compacted without saving")
141165
.option("--output <path>", "Output path for compacted trajectory")
142166
.action(async (options: CompactCommandOptions) => {
@@ -193,7 +217,15 @@ export function registerCompactCommand(program: Command): void {
193217
outputPath,
194218
markdownEnabled,
195219
);
196-
await markTrajectoriesAsCompacted(trajectories, mechanicalCompacted.id);
220+
if (options.discardSources) {
221+
const discardSummary = discardSourceTrajectories(trajectories);
222+
printDiscardSummary(discardSummary);
223+
} else {
224+
await markTrajectoriesAsCompacted(
225+
trajectories,
226+
mechanicalCompacted.id,
227+
);
228+
}
197229

198230
console.log(`\nCompacted trajectory saved to: ${outputPath}`);
199231
if (markdownEnabled) {
@@ -252,7 +284,12 @@ export function registerCompactCommand(program: Command): void {
252284
const outputPath =
253285
options.output || getDefaultOutputPath(compacted, options.workflow);
254286
saveCompactionArtifacts(compacted, outputPath, markdownEnabled);
255-
await markTrajectoriesAsCompacted(trajectories, compacted.id);
287+
if (options.discardSources) {
288+
const discardSummary = discardSourceTrajectories(trajectories);
289+
printDiscardSummary(discardSummary);
290+
} else {
291+
await markTrajectoriesAsCompacted(trajectories, compacted.id);
292+
}
256293

257294
console.log(`\nCompacted trajectory saved to: ${outputPath}`);
258295
if (markdownEnabled) {
@@ -441,9 +478,7 @@ function getCompactedTrajectoryIds(): Set<string> {
441478

442479
try {
443480
const indexContent = readFileSync(indexPath, "utf-8");
444-
const index = JSON.parse(indexContent) as {
445-
trajectories: Record<string, IndexEntry>;
446-
};
481+
const index = JSON.parse(indexContent) as TrajectoryIndex;
447482

448483
for (const [id, entry] of Object.entries(index.trajectories || {})) {
449484
if (entry.compactedInto) {
@@ -473,11 +508,7 @@ async function markTrajectoriesAsCompacted(
473508

474509
try {
475510
const indexContent = readFileSync(indexPath, "utf-8");
476-
const index = JSON.parse(indexContent) as {
477-
version: number;
478-
lastUpdated: string;
479-
trajectories: Record<string, IndexEntry>;
480-
};
511+
const index = JSON.parse(indexContent) as TrajectoryIndex;
481512

482513
let updated = false;
483514
for (const traj of trajectories) {
@@ -497,6 +528,86 @@ async function markTrajectoriesAsCompacted(
497528
}
498529
}
499530

531+
/**
532+
* Remove raw source trajectories after a durable compacted artifact has
533+
* been written. This keeps compaction as the long-lived record and makes
534+
* the index reflect only material that should remain visible.
535+
*/
536+
function discardSourceTrajectories(
537+
trajectories: Trajectory[],
538+
): DiscardSourcesSummary {
539+
const sourceIds = new Set(trajectories.map((trajectory) => trajectory.id));
540+
const summary: DiscardSourcesSummary = {
541+
removedIndexEntries: 0,
542+
deletedJsonFiles: 0,
543+
deletedMarkdownFiles: 0,
544+
deletedTraceFiles: 0,
545+
};
546+
547+
for (const searchPath of getSearchPaths()) {
548+
const indexPath = join(searchPath, "index.json");
549+
if (!existsSync(indexPath)) continue;
550+
551+
let index: TrajectoryIndex;
552+
try {
553+
const indexContent = readFileSync(indexPath, "utf-8");
554+
index = JSON.parse(indexContent) as TrajectoryIndex;
555+
} catch {
556+
// Keep behavior consistent with markTrajectoriesAsCompacted: malformed
557+
// indexes are ignored instead of blocking an already-saved compaction.
558+
continue;
559+
}
560+
561+
let updated = false;
562+
for (const id of sourceIds) {
563+
const entry = index.trajectories[id];
564+
if (!entry) continue;
565+
566+
if (deleteFileIfExists(entry.path)) {
567+
summary.deletedJsonFiles += 1;
568+
}
569+
if (deleteFileIfExists(getMarkdownOutputPath(entry.path))) {
570+
summary.deletedMarkdownFiles += 1;
571+
}
572+
if (deleteFileIfExists(getTraceOutputPath(entry.path))) {
573+
summary.deletedTraceFiles += 1;
574+
}
575+
576+
delete index.trajectories[id];
577+
summary.removedIndexEntries += 1;
578+
updated = true;
579+
}
580+
581+
if (updated) {
582+
index.lastUpdated = new Date().toISOString();
583+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
584+
}
585+
}
586+
587+
return summary;
588+
}
589+
590+
function deleteFileIfExists(path: string): boolean {
591+
if (!existsSync(path)) {
592+
return false;
593+
}
594+
595+
unlinkSync(path);
596+
return true;
597+
}
598+
599+
function getTraceOutputPath(outputPath: string): string {
600+
return outputPath.endsWith(".json")
601+
? outputPath.slice(0, -".json".length).concat(".trace.json")
602+
: `${outputPath}.trace.json`;
603+
}
604+
605+
function printDiscardSummary(summary: DiscardSourcesSummary): void {
606+
console.log(
607+
`Discarded source trajectories: ${summary.removedIndexEntries} index entries, ${summary.deletedJsonFiles} JSON files, ${summary.deletedMarkdownFiles} Markdown files, ${summary.deletedTraceFiles} trace files`,
608+
);
609+
}
610+
500611
function parseRelativeDate(input: string): Date {
501612
// Handle relative dates like "7d", "2w", "1m"
502613
const match = input.match(/^(\d+)([dwmh])$/);

0 commit comments

Comments
 (0)