@@ -5005,6 +5005,99 @@ describe('Maka Pi TUI runner', () => {
50055005 ] ) ;
50065006 } ) ;
50075007
5008+ test ( 'a settled /goal pause does not suppress a later host-initiated pause notice' , async ( ) => {
5009+ const terminal = new FakeTerminal ( 160 , 24 ) ;
5010+ const driver = new SlashCommandDriver ( ) ;
5011+ driver . goal = armedGoal ;
5012+ // The host can answer the control RPC before the subscription push folds
5013+ // the transition; the suppression flag must settle with the command
5014+ // instead of lingering for the push handler.
5015+ driver . deferGoalControlPush = true ;
5016+ const run = runMakaPiTui ( {
5017+ title : 'Maka' ,
5018+ driver,
5019+ cwd : '/repo' ,
5020+ model : 'claude-sonnet-4-5' ,
5021+ connectionSlug : 'claude-subscription' ,
5022+ permissionMode : 'ask' ,
5023+ terminal,
5024+ } ) ;
5025+ await waitFor ( ( ) => plainTerminalOutput ( terminal . output ( ) ) . includes ( 'goal 2/50' ) ) ;
5026+
5027+ terminal . input ( '/goal pause' ) ;
5028+ terminal . input ( '\r' ) ;
5029+ await waitFor ( ( ) =>
5030+ plainTerminalOutput ( terminal . output ( ) ) . includes ( 'Goal paused. /goal resume continues it' ) ,
5031+ ) ;
5032+
5033+ // The trailing push of the command's own pause folds onto the settled
5034+ // projection: no duplicate auto-pause notice.
5035+ driver . pushGoal ( { ...armedGoal , status : 'paused' , revision : 4 , pausedAt : Date . now ( ) } ) ;
5036+ await waitFor ( ( ) => plainTerminalOutput ( terminal . output ( ) ) . includes ( 'goal paused 2/50' ) ) ;
5037+ assert . equal (
5038+ plainTerminalOutput ( terminal . output ( ) ) . includes ( 'Goal paused (2/50).' ) ,
5039+ false ,
5040+ ) ;
5041+
5042+ terminal . input ( '/goal resume' ) ;
5043+ terminal . input ( '\r' ) ;
5044+ await waitFor ( ( ) => plainTerminalOutput ( terminal . output ( ) ) . includes ( 'Goal resumed.' ) ) ;
5045+
5046+ // A later host-initiated pause of the same goal (e.g. the Ctrl+C
5047+ // auto-pause) must announce itself.
5048+ driver . pushGoal ( {
5049+ ...armedGoal ,
5050+ status : 'paused' ,
5051+ revision : 6 ,
5052+ pausedAt : Date . now ( ) ,
5053+ lastReason : 'Goal-associated turn was aborted.' ,
5054+ } ) ;
5055+ await waitFor ( ( ) =>
5056+ plainTerminalOutput ( terminal . output ( ) ) . includes (
5057+ 'Goal paused (2/50). Goal-associated turn was aborted.' ,
5058+ ) ,
5059+ ) ;
5060+
5061+ exitMaka ( terminal ) ;
5062+ await Promise . race ( [
5063+ run ,
5064+ delay ( CLOSE_BUDGET_MS ) . then ( ( ) => {
5065+ throw new Error ( 'TUI did not close during test cleanup' ) ;
5066+ } ) ,
5067+ ] ) ;
5068+ } ) ;
5069+
5070+ test ( 'resuming into a session with a live goal announces the auto-continuing loop' , async ( ) => {
5071+ const terminal = new FakeTerminal ( 160 , 24 ) ;
5072+ const driver = new SlashCommandDriver ( [ fakeSessionSummary ( 'session-2' , '/repo' ) ] ) ;
5073+ // Before the switch, the driver has no attached session — the init-time
5074+ // check sees nothing; the notice must come from the switch seam.
5075+ driver . goal = null ;
5076+ driver . goalsBySessionId . set ( 'session-2' , armedGoal ) ;
5077+ const run = runMakaPiTui ( {
5078+ title : 'Maka' ,
5079+ driver,
5080+ cwd : '/repo' ,
5081+ model : 'claude-sonnet-4-5' ,
5082+ connectionSlug : 'claude-subscription' ,
5083+ permissionMode : 'ask' ,
5084+ terminal,
5085+ resumeSessionId : 'session-2' ,
5086+ } ) ;
5087+
5088+ await waitFor ( ( ) =>
5089+ plainTerminalOutput ( terminal . output ( ) ) . includes ( 'Autonomous goal is running (2/50)' ) ,
5090+ ) ;
5091+
5092+ exitMaka ( terminal ) ;
5093+ await Promise . race ( [
5094+ run ,
5095+ delay ( CLOSE_BUDGET_MS ) . then ( ( ) => {
5096+ throw new Error ( 'TUI did not close during test cleanup' ) ;
5097+ } ) ,
5098+ ] ) ;
5099+ } ) ;
5100+
50085101 test ( '/goal control pre-validates impossible transitions' , async ( ) => {
50095102 const terminal = new FakeTerminal ( ) ;
50105103 const driver = new SlashCommandDriver ( ) ;
@@ -6264,6 +6357,13 @@ class SlashCommandDriver implements MakaSessionDriver {
62646357
62656358 /** Records control actions and applies them to the local goal like the host would. */
62666359 readonly controlledGoalActions : Array < 'pause' | 'resume' | 'clear' > = [ ] ;
6360+ /**
6361+ * When true, controlGoal resolves without pushing the projection first —
6362+ * the response-before-push ordering a slow subscription stream can produce.
6363+ */
6364+ deferGoalControlPush = false ;
6365+ /** Per-session goal projections applied when switchSession adopts a session. */
6366+ readonly goalsBySessionId = new Map < string , GoalProjection | null > ( ) ;
62676367
62686368 controlGoal ( action : 'pause' | 'resume' | 'clear' ) : Promise < GoalProjection | null > {
62696369 this . controlledGoalActions . push ( action ) ;
@@ -6275,7 +6375,11 @@ class SlashCommandDriver implements MakaSessionDriver {
62756375 : action === 'pause'
62766376 ? { ...goal , status : 'paused' , revision : goal . revision + 1 , pausedAt : Date . now ( ) }
62776377 : { ...goal , status : 'active' , revision : goal . revision + 1 , pausedAt : null } ;
6278- this . pushGoal ( next ) ;
6378+ if ( this . deferGoalControlPush ) {
6379+ this . goal = next ;
6380+ } else {
6381+ this . pushGoal ( next ) ;
6382+ }
62796383 return Promise . resolve ( next ) ;
62806384 }
62816385
@@ -6381,6 +6485,9 @@ class SlashCommandDriver implements MakaSessionDriver {
63816485 const nextSummary = summary ?? fakeSessionSummary ( sessionId ) ;
63826486 this . orchestrationMode = nextSummary . orchestrationMode ?? 'default' ;
63836487 this . activeBoundaryDisplayMode = this . boundaryDisplayModeBySession . get ( nextSummary . id ) ;
6488+ if ( this . goalsBySessionId . has ( sessionId ) ) {
6489+ this . goal = this . goalsBySessionId . get ( sessionId ) ?? null ;
6490+ }
63846491 return switchResult ( nextSummary , [ ...( this . sessionMessages . get ( nextSummary . id ) ?? [ ] ) ] ) ;
63856492 }
63866493 async listRewindTargets ( ) : Promise < RewindTarget [ ] > {
0 commit comments