@@ -1195,7 +1195,7 @@ it.layer(
11951195
11961196 it . effect ( "keeps reading and deleting history at an encoded path that already fit" , ( ) =>
11971197 Effect . gen ( function * ( ) {
1198- const threadId = "x" . repeat ( 120 ) ;
1198+ const threadId = "x" . repeat ( 48 ) ;
11991199 const { manager, logsDir } = yield * createManager ( ) ;
12001200 const fs = yield * FileSystem . FileSystem ;
12011201 const logPath = yield * historyLogPath ( logsDir , threadId ) ;
@@ -1213,6 +1213,180 @@ it.layer(
12131213 } ) ,
12141214 ) ;
12151215
1216+ it . effect . each ( [
1217+ {
1218+ label : "delegated setup" ,
1219+ threadId :
1220+ "thread:delegated-task:command%3Amcp%3A00000000-0000-4000-8000-000000000000%3Adelegate-task%3Aexample-r4-workspace-prepare-engine3" ,
1221+ terminalId : "setup-setup-worktree-2" ,
1222+ } ,
1223+ { label : "long thread" , threadId : "t" . repeat ( 400 ) , terminalId : DEFAULT_TERMINAL_ID } ,
1224+ { label : "Unicode thread" , threadId : "项目😀" . repeat ( 60 ) , terminalId : "setup" } ,
1225+ { label : "long terminal" , threadId : "short" , terminalId : "t" . repeat ( 128 ) } ,
1226+ { label : "142-byte boundary" , threadId : "t" . repeat ( 48 ) , terminalId : "s" . repeat ( 48 ) } ,
1227+ { label : "144-byte boundary" , threadId : "t" . repeat ( 49 ) , terminalId : "s" . repeat ( 48 ) } ,
1228+ ] ) ( "persists $label history with a 143-byte filename limit" , ( { threadId, terminalId } ) =>
1229+ Effect . gen ( function * ( ) {
1230+ const fs = yield * FileSystem . FileSystem ;
1231+ const path = yield * Path . Path ;
1232+ const check = ( filePath : string ) =>
1233+ Buffer . byteLength ( path . basename ( filePath ) ) <= 143
1234+ ? Effect . void
1235+ : Effect . fail (
1236+ PlatformError . systemError ( {
1237+ _tag : "Unknown" ,
1238+ module : "FileSystem" ,
1239+ method : "access" ,
1240+ pathOrDescriptor : filePath ,
1241+ cause : { code : "ENAMETOOLONG" } ,
1242+ } ) ,
1243+ ) ;
1244+ const limited = FileSystem . FileSystem . of ( {
1245+ ...fs ,
1246+ exists : ( filePath ) => Effect . andThen ( check ( filePath ) , fs . exists ( filePath ) ) ,
1247+ open : ( filePath , options ) => Effect . andThen ( check ( filePath ) , fs . open ( filePath , options ) ) ,
1248+ writeFileString : ( filePath , data , options ) =>
1249+ Effect . andThen ( check ( filePath ) , fs . writeFileString ( filePath , data , options ) ) ,
1250+ remove : ( filePath , options ) =>
1251+ Effect . andThen ( check ( filePath ) , fs . remove ( filePath , options ) ) ,
1252+ } ) ;
1253+ const { manager, ptyAdapter, logsDir } = yield * createManager ( ) . pipe (
1254+ Effect . provideService ( FileSystem . FileSystem , limited ) ,
1255+ ) ;
1256+ yield * manager . open ( openInput ( { threadId, terminalId } ) ) ;
1257+ ptyAdapter . processes [ 0 ] ! . emitData ( "setup output\n" ) ;
1258+ yield * manager . close ( { threadId, terminalId } ) ;
1259+ const names = yield * fs . readDirectory ( logsDir ) ;
1260+ expect ( names ) . toHaveLength ( 1 ) ;
1261+ expect ( Buffer . byteLength ( names [ 0 ] ! ) ) . toBeLessThanOrEqual ( 143 ) ;
1262+ expect ( ( yield * manager . open ( openInput ( { threadId, terminalId } ) ) ) . history ) . toBe (
1263+ "setup output\n" ,
1264+ ) ;
1265+ const otherThread = `${ threadId } -other` ;
1266+ expect ( ( yield * manager . open ( openInput ( { threadId : otherThread , terminalId } ) ) ) . history ) . toBe (
1267+ "" ,
1268+ ) ;
1269+ ptyAdapter . processes [ 2 ] ! . emitData ( "other output\n" ) ;
1270+ yield * manager . close ( { threadId : otherThread , terminalId } ) ;
1271+ expect ( yield * fs . readDirectory ( logsDir ) ) . toHaveLength ( 2 ) ;
1272+ yield * manager . close ( { threadId, terminalId, deleteHistory : true } ) ;
1273+ expect ( yield * fs . readDirectory ( logsDir ) ) . toHaveLength ( 1 ) ;
1274+ yield * manager . close ( { threadId : otherThread , deleteHistory : true } ) ;
1275+ expect ( yield * fs . readDirectory ( logsDir ) ) . toEqual ( [ ] ) ;
1276+ } ) ,
1277+ ) ;
1278+
1279+ it . effect . each ( [
1280+ { source : "encoded" , terminalId : DEFAULT_TERMINAL_ID } ,
1281+ { source : "encoded" , terminalId : "setup-setup-worktree-2" } ,
1282+ { source : "sanitized" , terminalId : DEFAULT_TERMINAL_ID } ,
1283+ ] ) ( "migrates oversized $source history for $terminalId" , ( { source, terminalId } ) =>
1284+ Effect . gen ( function * ( ) {
1285+ const fs = yield * FileSystem . FileSystem ;
1286+ const path = yield * Path . Path ;
1287+ const { manager, logsDir } = yield * createManager ( ) ;
1288+ const threadId = "m" . repeat ( 100 ) ;
1289+ const oldPath =
1290+ source === "encoded"
1291+ ? path . join (
1292+ logsDir ,
1293+ `terminal_${ Base64Url . encode ( threadId ) } ${ terminalId === DEFAULT_TERMINAL_ID ? "" : `_${ Base64Url . encode ( terminalId ) } ` } .log` ,
1294+ )
1295+ : path . join ( logsDir , `${ threadId } .log` ) ;
1296+ yield * fs . writeFileString ( oldPath , "old output\n" ) ;
1297+ expect ( ( yield * manager . open ( openInput ( { threadId, terminalId } ) ) ) . history ) . toBe (
1298+ "old output\n" ,
1299+ ) ;
1300+ const names = yield * fs . readDirectory ( logsDir ) ;
1301+ expect ( names ) . toHaveLength ( 1 ) ;
1302+ expect ( Buffer . byteLength ( names [ 0 ] ! ) ) . toBeLessThanOrEqual ( 143 ) ;
1303+ expect ( yield * fs . exists ( oldPath ) ) . toBe ( false ) ;
1304+ expect ( yield * fs . readFileString ( path . join ( logsDir , names [ 0 ] ! ) ) ) . toBe ( "old output\n" ) ;
1305+ yield * manager . close ( { threadId, terminalId, deleteHistory : true } ) ;
1306+ expect ( yield * fs . readDirectory ( logsDir ) ) . toEqual ( [ ] ) ;
1307+ } ) ,
1308+ ) ;
1309+
1310+ it . effect . each ( [ "terminal" , "thread" ] as const ) (
1311+ "deletes compact and older encoded history when closing a %s" ,
1312+ ( scope ) =>
1313+ Effect . gen ( function * ( ) {
1314+ const fs = yield * FileSystem . FileSystem ;
1315+ const path = yield * Path . Path ;
1316+ const { manager, logsDir } = yield * createManager ( ) ;
1317+ const threadId = "d" . repeat ( 100 ) ;
1318+ const terminalId = "setup-setup-worktree-2" ;
1319+ yield * manager . open ( openInput ( { threadId, terminalId } ) ) ;
1320+ yield * manager . close ( { threadId, terminalId } ) ;
1321+ const oldPath = path . join (
1322+ logsDir ,
1323+ `terminal_${ Base64Url . encode ( threadId ) } _${ Base64Url . encode ( terminalId ) } .log` ,
1324+ ) ;
1325+ yield * fs . writeFileString ( oldPath , "stale copy\n" ) ;
1326+ yield * fs . writeFileString ( path . join ( logsDir , "unrelated.log" ) , "keep\n" ) ;
1327+ if ( scope === "terminal" ) {
1328+ yield * manager . close ( { threadId, terminalId, deleteHistory : true } ) ;
1329+ } else {
1330+ yield * manager . close ( { threadId, deleteHistory : true } ) ;
1331+ }
1332+ expect ( yield * fs . readDirectory ( logsDir ) ) . toEqual ( [ "unrelated.log" ] ) ;
1333+ } ) ,
1334+ ) ;
1335+
1336+ it . effect ( "prefers bounded history over a stale encoded copy" , ( ) =>
1337+ Effect . gen ( function * ( ) {
1338+ const { manager, ptyAdapter, logsDir } = yield * createManager ( ) ;
1339+ const fs = yield * FileSystem . FileSystem ;
1340+ const threadId = "p" . repeat ( 100 ) ;
1341+ yield * manager . open ( openInput ( { threadId } ) ) ;
1342+ ptyAdapter . processes [ 0 ] ! . emitData ( "current\n" ) ;
1343+ yield * manager . close ( { threadId } ) ;
1344+ const oldPath = yield * historyLogPath ( logsDir , threadId ) ;
1345+ yield * fs . writeFileString ( oldPath , "stale\n" ) ;
1346+
1347+ expect ( ( yield * manager . open ( openInput ( { threadId } ) ) ) . history ) . toBe ( "current\n" ) ;
1348+ yield * manager . close ( { threadId, terminalId : DEFAULT_TERMINAL_ID , deleteHistory : true } ) ;
1349+ expect ( yield * fs . readDirectory ( logsDir ) ) . toEqual ( [ ] ) ;
1350+ } ) ,
1351+ ) ;
1352+
1353+ it . effect ( "retains encoded history if migration cannot write the bounded file" , ( ) =>
1354+ Effect . gen ( function * ( ) {
1355+ const fs = yield * FileSystem . FileSystem ;
1356+ let failNextWrite = true ;
1357+ const limited = FileSystem . FileSystem . of ( {
1358+ ...fs ,
1359+ writeFileString : ( filePath , data , options ) => {
1360+ if ( ! failNextWrite ) return fs . writeFileString ( filePath , data , options ) ;
1361+ failNextWrite = false ;
1362+ return fs . writeFileString ( filePath , "partial" ) . pipe (
1363+ Effect . andThen (
1364+ Effect . fail (
1365+ PlatformError . systemError ( {
1366+ _tag : "Unknown" ,
1367+ module : "FileSystem" ,
1368+ method : "writeFileString" ,
1369+ cause : { code : "ENOSPC" } ,
1370+ } ) ,
1371+ ) ,
1372+ ) ,
1373+ ) ;
1374+ } ,
1375+ } ) ;
1376+ const { manager, logsDir } = yield * createManager ( ) . pipe (
1377+ Effect . provideService ( FileSystem . FileSystem , limited ) ,
1378+ ) ;
1379+ const threadId = "f" . repeat ( 100 ) ;
1380+ const oldPath = yield * historyLogPath ( logsDir , threadId ) ;
1381+ yield * fs . writeFileString ( oldPath , "preserve me\n" ) ;
1382+
1383+ expect ( Exit . isFailure ( yield * Effect . exit ( manager . open ( openInput ( { threadId } ) ) ) ) ) . toBe ( true ) ;
1384+ expect ( yield * fs . readFileString ( oldPath ) ) . toBe ( "preserve me\n" ) ;
1385+ expect ( yield * fs . readDirectory ( logsDir ) ) . toHaveLength ( 1 ) ;
1386+ expect ( ( yield * manager . open ( openInput ( { threadId } ) ) ) . history ) . toBe ( "preserve me\n" ) ;
1387+ } ) ,
1388+ ) ;
1389+
12161390 it . effect ( "migrates legacy transcript filenames to terminal-scoped history path on open" , ( ) =>
12171391 Effect . gen ( function * ( ) {
12181392 const { manager, logsDir } = yield * createManager ( ) ;
0 commit comments