99 */
1010
1111import { 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" ;
1319import { dirname , join } from "node:path" ;
1420import type { Command } from "commander" ;
1521import { 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+
7890interface 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+
95115interface 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,103 @@ 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+ const parsedIndex = JSON . parse ( indexContent ) as unknown ;
555+ if ( ! isTrajectoryIndex ( parsedIndex ) ) {
556+ continue ;
557+ }
558+ index = parsedIndex ;
559+ } catch {
560+ // Keep behavior consistent with markTrajectoriesAsCompacted: malformed
561+ // indexes are ignored instead of blocking an already-saved compaction.
562+ continue ;
563+ }
564+
565+ let updated = false ;
566+ for ( const id of sourceIds ) {
567+ const entry = index . trajectories [ id ] ;
568+ if ( ! entry ) continue ;
569+
570+ if ( deleteFileIfExists ( entry . path ) ) {
571+ summary . deletedJsonFiles += 1 ;
572+ }
573+ if ( deleteFileIfExists ( getMarkdownOutputPath ( entry . path ) ) ) {
574+ summary . deletedMarkdownFiles += 1 ;
575+ }
576+ if ( deleteFileIfExists ( getTraceOutputPath ( entry . path ) ) ) {
577+ summary . deletedTraceFiles += 1 ;
578+ }
579+
580+ delete index . trajectories [ id ] ;
581+ summary . removedIndexEntries += 1 ;
582+ updated = true ;
583+ }
584+
585+ if ( updated ) {
586+ index . lastUpdated = new Date ( ) . toISOString ( ) ;
587+ writeFileSync ( indexPath , JSON . stringify ( index , null , 2 ) ) ;
588+ }
589+ }
590+
591+ return summary ;
592+ }
593+
594+ function deleteFileIfExists ( path : string ) : boolean {
595+ if ( ! existsSync ( path ) ) {
596+ return false ;
597+ }
598+
599+ unlinkSync ( path ) ;
600+ return true ;
601+ }
602+
603+ function isTrajectoryIndex ( value : unknown ) : value is TrajectoryIndex {
604+ if ( value === null || typeof value !== "object" ) {
605+ return false ;
606+ }
607+
608+ const candidate = value as Partial < TrajectoryIndex > ;
609+ return (
610+ candidate . trajectories !== null &&
611+ typeof candidate . trajectories === "object" &&
612+ ! Array . isArray ( candidate . trajectories )
613+ ) ;
614+ }
615+
616+ function getTraceOutputPath ( outputPath : string ) : string {
617+ return outputPath . endsWith ( ".json" )
618+ ? outputPath . slice ( 0 , - ".json" . length ) . concat ( ".trace.json" )
619+ : `${ outputPath } .trace.json` ;
620+ }
621+
622+ function printDiscardSummary ( summary : DiscardSourcesSummary ) : void {
623+ console . log (
624+ `Discarded source trajectories: ${ summary . removedIndexEntries } index entries, ${ summary . deletedJsonFiles } JSON files, ${ summary . deletedMarkdownFiles } Markdown files, ${ summary . deletedTraceFiles } trace files` ,
625+ ) ;
626+ }
627+
500628function parseRelativeDate ( input : string ) : Date {
501629 // Handle relative dates like "7d", "2w", "1m"
502630 const match = input . match ( / ^ ( \d + ) ( [ d w m h ] ) $ / ) ;
0 commit comments