@@ -633,6 +633,20 @@ async function approveMcpLaunch(ctx, server, dbg) {
633633 return true ;
634634}
635635
636+ /**
637+ * What we last TOLD the user about a run's context (rules / memory / MCP).
638+ *
639+ * The context itself is rebuilt every run — that is deliberate, a run's servers are whatever is
640+ * configured and reachable right now. Re-ANNOUNCING it every turn is different, and it was noise:
641+ * three identical rows at the top of every single answer, saying the same thing they said last time.
642+ *
643+ * A signature, not a boolean, because the announcement has to come back the moment anything moves —
644+ * a server dropping out, a rules file appearing, memory arriving for the first time. Silence is only
645+ * correct while the picture is unchanged.
646+ */
647+ let lastContextSig = '' ;
648+ function resetContextAnnounce ( ) { lastContextSig = '' ; }
649+
636650async function setupMcp ( ctx , wsFolders , dbg ) {
637651 const empty = { tools : [ ] , routes : null } ;
638652 const cfg = ctx . mcp || { } ;
@@ -687,7 +701,9 @@ async function setupMcp(ctx, wsFolders, dbg) {
687701 const perServer = toolCountsByServer ( built . routes ) ;
688702 const summary = handles . map ( ( h ) => h . name + ' (' + ( perServer . get ( h . name ) || 0 ) + ')' ) . join ( ', ' ) ;
689703 dbg ( 'mcp.ready' , { servers : handles . map ( ( h ) => h . name ) , tools : built . tools . length , allowed } ) ;
690- ctx . post ( { type : 'agentTool' , icon : 'sparkle' , text : '🔌 mcp · ' + summary + ' · ' + allowed + '/' + built . tools . length + ' allow-listed' } ) ;
704+ // Handed back rather than posted: runAgent decides whether the user needs to hear it again.
705+ // Failures below still post immediately — a server that broke is news every time.
706+ built . announce = { type : 'agentTool' , icon : 'sparkle' , text : '🔌 mcp · ' + summary + ' · ' + allowed + '/' + built . tools . length + ' allow-listed' } ;
691707 return built ;
692708 } catch ( e ) {
693709 dbg ( 'mcp.failed' , { error : ( e && e . message ) || String ( e ) } ) ;
@@ -739,21 +755,36 @@ async function runAgent(ctx) {
739755 const systemTokensEst = Math . round ( system . length / 4 ) ;
740756
741757 const dbg = ctx . dbg || ( ( ) => { } ) ;
758+ // The run's context, COLLECTED rather than posted. Whether the user needs to see it again is a
759+ // question about the whole picture, and the MCP part of that picture is not known until setupMcp
760+ // has run — so nothing is announced until all three are in hand.
761+ const contextChips = [ ] ;
742762 if ( rules . sources . length ) {
743763 dbg ( 'projectRules.loaded' , { sources : rules . sources } ) ;
744- // Quiet timeline chip at the top of the run so the user can see their repo rules are in effect
745- // (mirrors the skill chip). Reuses the agentTool → addAgentLine rendering — no webview change.
746- ctx . post ( { type : 'agentTool' , icon : 'file' , text : '📋 project rules · ' + rules . sources . join ( ', ' ) } ) ;
764+ contextChips . push ( { type : 'agentTool' , icon : 'file' , text : '📋 project rules · ' + rules . sources . join ( ', ' ) } ) ;
747765 }
748766 if ( ctx . projectMemory ) {
749767 dbg ( 'projectMemory.loaded' , { chars : ctx . projectMemory . length } ) ;
750- ctx . post ( { type : 'agentTool' , icon : 'history' , text : '🧠 project memory' } ) ;
768+ contextChips . push ( { type : 'agentTool' , icon : 'history' , text : '🧠 project memory' } ) ;
751769 }
752770
753771 // MCP (docs/MCP.md S3): the tool list becomes PER-RUN. It was a module constant only because it was
754772 // the same every time; a run's servers are whatever is configured and reachable right now. Same shape
755773 // as `system`/`systemTokensEst` two lines up — built once per run, then used for every turn.
756774 const mcp = await setupMcp ( ctx , wsFolders , dbg ) ;
775+ if ( mcp . announce ) { contextChips . push ( mcp . announce ) ; }
776+
777+ // Say it only when it CHANGED. The context is rebuilt every run by design; repeating it at the top
778+ // of every answer is not the same thing, and three identical rows before each reply is noise the
779+ // reference transcript does not have. A signature rather than a flag, so the announcement returns
780+ // the moment a server drops, a rules file appears, or memory shows up for the first time.
781+ const sig = contextChips . map ( ( c ) => c . text ) . join ( '|' ) ;
782+ if ( sig && sig !== lastContextSig ) {
783+ lastContextSig = sig ;
784+ for ( const chip of contextChips ) { ctx . post ( chip ) ; }
785+ } else if ( ! sig ) {
786+ lastContextSig = '' ; // nothing to say now; say it again when there is
787+ }
757788 ctx . mcpRoutes = mcp . routes ; // runTool's router reads this
758789 // Rootless runs get the portable subset; MCP tools are unaffected either way.
759790 const builtins = root ? TOOLS : PORTABLE_TOOLS ;
@@ -1030,4 +1061,4 @@ async function runAgent(ctx) {
10301061 }
10311062}
10321063
1033- module . exports = { runAgent, makeDiff, resolveWorkspacePath } ;
1064+ module . exports = { resetContextAnnounce , runAgent, makeDiff, resolveWorkspacePath } ;
0 commit comments