@@ -21,6 +21,8 @@ import {
2121 getSessionDetails as getAgentSessionDetails ,
2222 getSessionMessages ,
2323 findSessionMessages ,
24+ deleteSession as deleteSessionFromProvider ,
25+ searchSessions as searchSessionsInContainer ,
2426} from '../sessions/agents' ;
2527import type { SessionsCacheManager } from '../sessions/cache' ;
2628import type { ModelCacheManager } from '../models/cache' ;
@@ -32,6 +34,7 @@ import {
3234import {
3335 listOpencodeSessions ,
3436 getOpencodeSessionMessages ,
37+ deleteOpencodeSession ,
3538} from '../sessions/agents/opencode-storage' ;
3639
3740const WorkspaceStatusSchema = z . enum ( [ 'running' , 'stopped' , 'creating' , 'error' ] ) ;
@@ -708,6 +711,245 @@ export function createRouter(ctx: RouterContext) {
708711 return { success : true } ;
709712 } ) ;
710713
714+ const deleteSession = os
715+ . input (
716+ z . object ( {
717+ workspaceName : z . string ( ) ,
718+ sessionId : z . string ( ) ,
719+ agentType : z . enum ( [ 'claude-code' , 'opencode' , 'codex' ] ) ,
720+ } )
721+ )
722+ . handler ( async ( { input } ) => {
723+ const isHost = input . workspaceName === HOST_WORKSPACE_NAME ;
724+
725+ if ( isHost ) {
726+ const config = ctx . config . get ( ) ;
727+ if ( ! config . allowHostAccess ) {
728+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Host access is disabled' } ) ;
729+ }
730+
731+ const result = await deleteHostSession ( input . sessionId , input . agentType ) ;
732+ if ( ! result . success ) {
733+ throw new ORPCError ( 'INTERNAL_SERVER_ERROR' , {
734+ message : result . error || 'Failed to delete session' ,
735+ } ) ;
736+ }
737+
738+ await deleteSessionName ( ctx . stateDir , input . workspaceName , input . sessionId ) ;
739+ await ctx . sessionsCache . removeSession ( input . workspaceName , input . sessionId ) ;
740+
741+ return { success : true } ;
742+ }
743+
744+ const workspace = await ctx . workspaces . get ( input . workspaceName ) ;
745+ if ( ! workspace ) {
746+ throw new ORPCError ( 'NOT_FOUND' , { message : 'Workspace not found' } ) ;
747+ }
748+ if ( workspace . status !== 'running' ) {
749+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Workspace is not running' } ) ;
750+ }
751+
752+ const containerName = `workspace-${ input . workspaceName } ` ;
753+
754+ const result = await deleteSessionFromProvider (
755+ containerName ,
756+ input . sessionId ,
757+ input . agentType ,
758+ execInContainer
759+ ) ;
760+
761+ if ( ! result . success ) {
762+ throw new ORPCError ( 'INTERNAL_SERVER_ERROR' , {
763+ message : result . error || 'Failed to delete session' ,
764+ } ) ;
765+ }
766+
767+ await deleteSessionName ( ctx . stateDir , input . workspaceName , input . sessionId ) ;
768+ await ctx . sessionsCache . removeSession ( input . workspaceName , input . sessionId ) ;
769+
770+ return { success : true } ;
771+ } ) ;
772+
773+ const searchSessions = os
774+ . input (
775+ z . object ( {
776+ workspaceName : z . string ( ) ,
777+ query : z . string ( ) . min ( 1 ) . max ( 500 ) ,
778+ } )
779+ )
780+ . handler ( async ( { input } ) => {
781+ const isHost = input . workspaceName === HOST_WORKSPACE_NAME ;
782+
783+ if ( isHost ) {
784+ const config = ctx . config . get ( ) ;
785+ if ( ! config . allowHostAccess ) {
786+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Host access is disabled' } ) ;
787+ }
788+
789+ const results = await searchHostSessions ( input . query ) ;
790+ return { results } ;
791+ }
792+
793+ const workspace = await ctx . workspaces . get ( input . workspaceName ) ;
794+ if ( ! workspace ) {
795+ throw new ORPCError ( 'NOT_FOUND' , { message : 'Workspace not found' } ) ;
796+ }
797+ if ( workspace . status !== 'running' ) {
798+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Workspace is not running' } ) ;
799+ }
800+
801+ const containerName = `workspace-${ input . workspaceName } ` ;
802+ const results = await searchSessionsInContainer ( containerName , input . query , execInContainer ) ;
803+
804+ return { results } ;
805+ } ) ;
806+
807+ async function searchHostSessions ( query : string ) : Promise <
808+ Array < {
809+ sessionId : string ;
810+ agentType : 'claude-code' | 'opencode' | 'codex' ;
811+ matchCount : number ;
812+ } >
813+ > {
814+ const homeDir = os_module . homedir ( ) ;
815+ const safeQuery = query . replace ( / [ ' " \\ ] / g, '\\$&' ) ;
816+ const searchPaths = [
817+ path . join ( homeDir , '.claude' , 'projects' ) ,
818+ path . join ( homeDir , '.local' , 'share' , 'opencode' , 'storage' ) ,
819+ path . join ( homeDir , '.codex' , 'sessions' ) ,
820+ ] . filter ( ( p ) => {
821+ try {
822+ require ( 'fs' ) . accessSync ( p ) ;
823+ return true ;
824+ } catch {
825+ return false ;
826+ }
827+ } ) ;
828+
829+ if ( searchPaths . length === 0 ) {
830+ return [ ] ;
831+ }
832+
833+ const { execSync } = await import ( 'child_process' ) ;
834+ try {
835+ const output = execSync (
836+ `rg -l -i --no-messages "${ safeQuery } " ${ searchPaths . join ( ' ' ) } 2>/dev/null | head -100` ,
837+ {
838+ encoding : 'utf-8' ,
839+ timeout : 30000 ,
840+ }
841+ ) ;
842+
843+ const files = output . trim ( ) . split ( '\n' ) . filter ( Boolean ) ;
844+ const results : Array < {
845+ sessionId : string ;
846+ agentType : 'claude-code' | 'opencode' | 'codex' ;
847+ matchCount : number ;
848+ } > = [ ] ;
849+
850+ for ( const file of files ) {
851+ let sessionId : string | null = null ;
852+ let agentType : 'claude-code' | 'opencode' | 'codex' | null = null ;
853+
854+ if ( file . includes ( '/.claude/projects/' ) ) {
855+ const match = file . match ( / \/ ( [ ^ / ] + ) \. j s o n l $ / ) ;
856+ if ( match && ! match [ 1 ] . startsWith ( 'agent-' ) ) {
857+ sessionId = match [ 1 ] ;
858+ agentType = 'claude-code' ;
859+ }
860+ } else if ( file . includes ( '/.local/share/opencode/storage/' ) ) {
861+ if ( file . includes ( '/session/' ) && file . endsWith ( '.json' ) ) {
862+ const match = file . match ( / \/ ( s e s _ [ ^ / ] + ) \. j s o n $ / ) ;
863+ if ( match ) {
864+ sessionId = match [ 1 ] ;
865+ agentType = 'opencode' ;
866+ }
867+ }
868+ } else if ( file . includes ( '/.codex/sessions/' ) ) {
869+ const match = file . match ( / \/ ( [ ^ / ] + ) \. j s o n l $ / ) ;
870+ if ( match ) {
871+ sessionId = match [ 1 ] ;
872+ agentType = 'codex' ;
873+ }
874+ }
875+
876+ if ( sessionId && agentType ) {
877+ results . push ( { sessionId, agentType, matchCount : 1 } ) ;
878+ }
879+ }
880+
881+ return results ;
882+ } catch {
883+ return [ ] ;
884+ }
885+ }
886+
887+ async function deleteHostSession (
888+ sessionId : string ,
889+ agentType : 'claude-code' | 'opencode' | 'codex'
890+ ) : Promise < { success : boolean ; error ?: string } > {
891+ const homeDir = os_module . homedir ( ) ;
892+
893+ if ( agentType === 'claude-code' ) {
894+ const safeSessionId = sessionId . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '' ) ;
895+ const claudeProjectsDir = path . join ( homeDir , '.claude' , 'projects' ) ;
896+
897+ try {
898+ const projectDirs = await fs . readdir ( claudeProjectsDir ) ;
899+ for ( const projectDir of projectDirs ) {
900+ const sessionFile = path . join ( claudeProjectsDir , projectDir , `${ safeSessionId } .jsonl` ) ;
901+ try {
902+ await fs . unlink ( sessionFile ) ;
903+ return { success : true } ;
904+ } catch {
905+ continue ;
906+ }
907+ }
908+ } catch {
909+ return { success : false , error : 'Session not found' } ;
910+ }
911+ return { success : false , error : 'Session not found' } ;
912+ }
913+
914+ if ( agentType === 'opencode' ) {
915+ return deleteOpencodeSession ( sessionId ) ;
916+ }
917+
918+ if ( agentType === 'codex' ) {
919+ const codexSessionsDir = path . join ( homeDir , '.codex' , 'sessions' ) ;
920+ try {
921+ const files = await fs . readdir ( codexSessionsDir ) ;
922+ for ( const file of files ) {
923+ if ( ! file . endsWith ( '.jsonl' ) ) continue ;
924+ const filePath = path . join ( codexSessionsDir , file ) ;
925+ const fileId = file . replace ( '.jsonl' , '' ) ;
926+
927+ if ( fileId === sessionId ) {
928+ await fs . unlink ( filePath ) ;
929+ return { success : true } ;
930+ }
931+
932+ try {
933+ const content = await fs . readFile ( filePath , 'utf-8' ) ;
934+ const firstLine = content . split ( '\n' ) [ 0 ] ;
935+ const meta = JSON . parse ( firstLine ) as { session_id ?: string } ;
936+ if ( meta . session_id === sessionId ) {
937+ await fs . unlink ( filePath ) ;
938+ return { success : true } ;
939+ }
940+ } catch {
941+ continue ;
942+ }
943+ }
944+ } catch {
945+ return { success : false , error : 'Session not found' } ;
946+ }
947+ return { success : false , error : 'Session not found' } ;
948+ }
949+
950+ return { success : false , error : 'Unsupported agent type' } ;
951+ }
952+
711953 const getHostInfo = os . handler ( async ( ) => {
712954 const config = ctx . config . get ( ) ;
713955 return {
@@ -813,6 +1055,8 @@ export function createRouter(ctx: RouterContext) {
8131055 clearName : clearSessionName ,
8141056 getRecent : getRecentSessions ,
8151057 recordAccess : recordSessionAccess ,
1058+ delete : deleteSession ,
1059+ search : searchSessions ,
8161060 } ,
8171061 models : {
8181062 list : listModels ,
0 commit comments