@@ -21,6 +21,7 @@ import {
2121 getSessionDetails as getAgentSessionDetails ,
2222 getSessionMessages ,
2323 findSessionMessages ,
24+ deleteSession as deleteSessionFromProvider ,
2425} from '../sessions/agents' ;
2526import type { SessionsCacheManager } from '../sessions/cache' ;
2627import type { ModelCacheManager } from '../models/cache' ;
@@ -32,6 +33,7 @@ import {
3233import {
3334 listOpencodeSessions ,
3435 getOpencodeSessionMessages ,
36+ deleteOpencodeSession ,
3537} from '../sessions/agents/opencode-storage' ;
3638
3739const WorkspaceStatusSchema = z . enum ( [ 'running' , 'stopped' , 'creating' , 'error' ] ) ;
@@ -708,6 +710,127 @@ export function createRouter(ctx: RouterContext) {
708710 return { success : true } ;
709711 } ) ;
710712
713+ const deleteSession = os
714+ . input (
715+ z . object ( {
716+ workspaceName : z . string ( ) ,
717+ sessionId : z . string ( ) ,
718+ agentType : z . enum ( [ 'claude-code' , 'opencode' , 'codex' ] ) ,
719+ } )
720+ )
721+ . handler ( async ( { input } ) => {
722+ const isHost = input . workspaceName === HOST_WORKSPACE_NAME ;
723+
724+ if ( isHost ) {
725+ const config = ctx . config . get ( ) ;
726+ if ( ! config . allowHostAccess ) {
727+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Host access is disabled' } ) ;
728+ }
729+
730+ const result = await deleteHostSession ( input . sessionId , input . agentType ) ;
731+ if ( ! result . success ) {
732+ throw new ORPCError ( 'INTERNAL_SERVER_ERROR' , { message : result . error || 'Failed to delete session' } ) ;
733+ }
734+
735+ await deleteSessionName ( ctx . stateDir , input . workspaceName , input . sessionId ) ;
736+ await ctx . sessionsCache . removeSession ( input . workspaceName , input . sessionId ) ;
737+
738+ return { success : true } ;
739+ }
740+
741+ const workspace = await ctx . workspaces . get ( input . workspaceName ) ;
742+ if ( ! workspace ) {
743+ throw new ORPCError ( 'NOT_FOUND' , { message : 'Workspace not found' } ) ;
744+ }
745+ if ( workspace . status !== 'running' ) {
746+ throw new ORPCError ( 'PRECONDITION_FAILED' , { message : 'Workspace is not running' } ) ;
747+ }
748+
749+ const containerName = `workspace-${ input . workspaceName } ` ;
750+
751+ const result = await deleteSessionFromProvider (
752+ containerName ,
753+ input . sessionId ,
754+ input . agentType ,
755+ execInContainer
756+ ) ;
757+
758+ if ( ! result . success ) {
759+ throw new ORPCError ( 'INTERNAL_SERVER_ERROR' , { message : result . error || 'Failed to delete session' } ) ;
760+ }
761+
762+ await deleteSessionName ( ctx . stateDir , input . workspaceName , input . sessionId ) ;
763+ await ctx . sessionsCache . removeSession ( input . workspaceName , input . sessionId ) ;
764+
765+ return { success : true } ;
766+ } ) ;
767+
768+ async function deleteHostSession (
769+ sessionId : string ,
770+ agentType : 'claude-code' | 'opencode' | 'codex'
771+ ) : Promise < { success : boolean ; error ?: string } > {
772+ const homeDir = os_module . homedir ( ) ;
773+
774+ if ( agentType === 'claude-code' ) {
775+ const safeSessionId = sessionId . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '' ) ;
776+ const claudeProjectsDir = path . join ( homeDir , '.claude' , 'projects' ) ;
777+
778+ try {
779+ const projectDirs = await fs . readdir ( claudeProjectsDir ) ;
780+ for ( const projectDir of projectDirs ) {
781+ const sessionFile = path . join ( claudeProjectsDir , projectDir , `${ safeSessionId } .jsonl` ) ;
782+ try {
783+ await fs . unlink ( sessionFile ) ;
784+ return { success : true } ;
785+ } catch {
786+ continue ;
787+ }
788+ }
789+ } catch {
790+ return { success : false , error : 'Session not found' } ;
791+ }
792+ return { success : false , error : 'Session not found' } ;
793+ }
794+
795+ if ( agentType === 'opencode' ) {
796+ return deleteOpencodeSession ( sessionId ) ;
797+ }
798+
799+ if ( agentType === 'codex' ) {
800+ const codexSessionsDir = path . join ( homeDir , '.codex' , 'sessions' ) ;
801+ try {
802+ const files = await fs . readdir ( codexSessionsDir ) ;
803+ for ( const file of files ) {
804+ if ( ! file . endsWith ( '.jsonl' ) ) continue ;
805+ const filePath = path . join ( codexSessionsDir , file ) ;
806+ const fileId = file . replace ( '.jsonl' , '' ) ;
807+
808+ if ( fileId === sessionId ) {
809+ await fs . unlink ( filePath ) ;
810+ return { success : true } ;
811+ }
812+
813+ try {
814+ const content = await fs . readFile ( filePath , 'utf-8' ) ;
815+ const firstLine = content . split ( '\n' ) [ 0 ] ;
816+ const meta = JSON . parse ( firstLine ) as { session_id ?: string } ;
817+ if ( meta . session_id === sessionId ) {
818+ await fs . unlink ( filePath ) ;
819+ return { success : true } ;
820+ }
821+ } catch {
822+ continue ;
823+ }
824+ }
825+ } catch {
826+ return { success : false , error : 'Session not found' } ;
827+ }
828+ return { success : false , error : 'Session not found' } ;
829+ }
830+
831+ return { success : false , error : 'Unsupported agent type' } ;
832+ }
833+
711834 const getHostInfo = os . handler ( async ( ) => {
712835 const config = ctx . config . get ( ) ;
713836 return {
@@ -813,6 +936,7 @@ export function createRouter(ctx: RouterContext) {
813936 clearName : clearSessionName ,
814937 getRecent : getRecentSessions ,
815938 recordAccess : recordSessionAccess ,
939+ delete : deleteSession ,
816940 } ,
817941 models : {
818942 list : listModels ,
0 commit comments