@@ -2289,6 +2289,101 @@ describe("cleanup", () => {
22892289 expect ( result . skipped ) . toContain ( "app-orchestrator" ) ;
22902290 } ) ;
22912291
2292+ it ( "never cleans the canonical orchestrator session even with stale worker-like metadata" , async ( ) => {
2293+ const deleteLogPath = join ( tmpDir , "opencode-delete-orchestrator.log" ) ;
2294+ const mockBin = installMockOpencode ( "[]" , deleteLogPath ) ;
2295+ process . env . PATH = `${ mockBin } :${ originalPath ?? "" } ` ;
2296+
2297+ const deadRuntime : Runtime = {
2298+ ...mockRuntime ,
2299+ isAlive : vi . fn ( ) . mockResolvedValue ( false ) ,
2300+ } ;
2301+ const mockSCM : SCM = {
2302+ name : "mock-scm" ,
2303+ detectPR : vi . fn ( ) ,
2304+ getPRState : vi . fn ( ) . mockResolvedValue ( "merged" ) ,
2305+ mergePR : vi . fn ( ) ,
2306+ closePR : vi . fn ( ) ,
2307+ getCIChecks : vi . fn ( ) ,
2308+ getCISummary : vi . fn ( ) ,
2309+ getReviews : vi . fn ( ) ,
2310+ getReviewDecision : vi . fn ( ) ,
2311+ getPendingComments : vi . fn ( ) ,
2312+ getAutomatedComments : vi . fn ( ) ,
2313+ getMergeability : vi . fn ( ) ,
2314+ } ;
2315+ const mockTracker : Tracker = {
2316+ name : "mock-tracker" ,
2317+ getIssue : vi . fn ( ) . mockResolvedValue ( {
2318+ id : "INT-42" ,
2319+ title : "Issue" ,
2320+ description : "" ,
2321+ url : "https://example.com/INT-42" ,
2322+ state : "closed" ,
2323+ labels : [ ] ,
2324+ } ) ,
2325+ isCompleted : vi . fn ( ) . mockResolvedValue ( true ) ,
2326+ issueUrl : vi . fn ( ) . mockReturnValue ( "https://example.com/INT-42" ) ,
2327+ branchName : vi . fn ( ) . mockReturnValue ( "feat/INT-42" ) ,
2328+ generatePrompt : vi . fn ( ) . mockResolvedValue ( "" ) ,
2329+ } ;
2330+ const registryWithSignals : PluginRegistry = {
2331+ ...mockRegistry ,
2332+ get : vi . fn ( ) . mockImplementation ( ( slot : string ) => {
2333+ if ( slot === "runtime" ) return deadRuntime ;
2334+ if ( slot === "agent" ) return mockAgent ;
2335+ if ( slot === "workspace" ) return mockWorkspace ;
2336+ if ( slot === "scm" ) return mockSCM ;
2337+ if ( slot === "tracker" ) return mockTracker ;
2338+ return null ;
2339+ } ) ,
2340+ } ;
2341+
2342+ writeMetadata ( sessionsDir , "app-orchestrator" , {
2343+ worktree : "/tmp" ,
2344+ branch : "main" ,
2345+ status : "ci_failed" ,
2346+ project : "my-app" ,
2347+ issue : "INT-42" ,
2348+ pr : "https://github.com/org/repo/pull/10" ,
2349+ agent : "opencode" ,
2350+ opencodeSessionId : "ses_orchestrator_active" ,
2351+ runtimeHandle : JSON . stringify ( makeHandle ( "rt-orchestrator" ) ) ,
2352+ } ) ;
2353+
2354+ const sm = createSessionManager ( { config, registry : registryWithSignals } ) ;
2355+ const result = await sm . cleanup ( ) ;
2356+
2357+ expect ( result . killed ) . not . toContain ( "app-orchestrator" ) ;
2358+ expect ( result . skipped ) . toContain ( "app-orchestrator" ) ;
2359+ expect ( existsSync ( deleteLogPath ) ) . toBe ( false ) ;
2360+ } ) ;
2361+
2362+ it ( "never cleans archived orchestrator mappings even when metadata looks stale" , async ( ) => {
2363+ const deleteLogPath = join ( tmpDir , "opencode-delete-archived-orchestrator.log" ) ;
2364+ const mockBin = installMockOpencode ( "[]" , deleteLogPath ) ;
2365+ process . env . PATH = `${ mockBin } :${ originalPath ?? "" } ` ;
2366+
2367+ writeMetadata ( sessionsDir , "app-orchestrator" , {
2368+ worktree : "/tmp" ,
2369+ branch : "main" ,
2370+ status : "killed" ,
2371+ project : "my-app" ,
2372+ agent : "opencode" ,
2373+ opencodeSessionId : "ses_orchestrator_archived" ,
2374+ pr : "https://github.com/org/repo/pull/88" ,
2375+ runtimeHandle : JSON . stringify ( makeHandle ( "rt-orchestrator" ) ) ,
2376+ } ) ;
2377+ deleteMetadata ( sessionsDir , "app-orchestrator" , true ) ;
2378+
2379+ const sm = createSessionManager ( { config, registry : mockRegistry } ) ;
2380+ const result = await sm . cleanup ( ) ;
2381+
2382+ expect ( result . killed ) . not . toContain ( "app-orchestrator" ) ;
2383+ expect ( result . skipped ) . toContain ( "app-orchestrator" ) ;
2384+ expect ( existsSync ( deleteLogPath ) ) . toBe ( false ) ;
2385+ } ) ;
2386+
22922387 it ( "kills sessions with dead runtimes" , async ( ) => {
22932388 const deadRuntime : Runtime = {
22942389 ...mockRuntime ,
0 commit comments