@@ -102,6 +102,9 @@ func logTokenDelta(beforeTokens int) {
102102func streamAndPrint (ctx context.Context , a * iteragent.Agent , prompt string , repoPath string ) {
103103 recordMessage ()
104104
105+ // Sync pinned messages into the agent before each request.
106+ a .SetPinnedMessages (getPinnedMessages ())
107+
105108 reqCtx , cancel := context .WithCancel (ctx )
106109 sess .RequestCancel = cancel
107110 defer func () {
@@ -118,9 +121,13 @@ func streamAndPrint(ctx context.Context, a *iteragent.Agent, prompt string, repo
118121
119122 var fullContent string
120123 var toolStart time.Time
124+ var ttft time.Duration
121125 beforeTokens := sess .Tokens
122126
123127 for e := range events {
128+ if ttft == 0 && iteragent .EventType (e .Type ) == iteragent .EventTokenUpdate && e .Content != "" {
129+ ttft = time .Since (start ).Round (time .Millisecond )
130+ }
124131 fullContent , toolStart = processStreamEvent (e , fullContent , toolStart , stopOnce , newSpinner , repoPath )
125132 }
126133 a .Finish ()
@@ -135,7 +142,12 @@ func streamAndPrint(ctx context.Context, a *iteragent.Agent, prompt string, repo
135142 elapsed := time .Since (start ).Round (time .Millisecond )
136143
137144 updateSessionTokens (a , fullContent )
138- printFinalStats (elapsed , beforeTokens , fullContent )
145+ printFinalStats (elapsed , ttft , beforeTokens , fullContent )
146+
147+ // Autosave after each turn so a crash doesn't lose the session.
148+ if len (a .Messages ) > 0 {
149+ _ = saveSession ("autosave" , a .Messages )
150+ }
139151}
140152
141153// newSpinnerController creates a spinner control pair (stopOnce, newSpinner).
@@ -147,6 +159,7 @@ func newSpinnerController() (func(), func(string)) {
147159 stopOnce func ()
148160 spinnerLabel string
149161 )
162+ stopOnce = func () {} // no-op until a spinner is started
150163 newSpinner := func (label string ) {
151164 spinnerLabel = label
152165 stopSpinner = make (chan struct {})
@@ -161,7 +174,9 @@ func newSpinnerController() (func(), func(string)) {
161174 go spinner (stopSpinner , spinnerDone , spinnerLabel )
162175 }
163176 _ = spinnerLabel
164- return stopOnce , newSpinner
177+ // Return a wrapper that always calls the *current* stopOnce, not the
178+ // initial no-op captured at return time.
179+ return func () { stopOnce () }, newSpinner
165180}
166181
167182// processStreamEvent handles a single agent stream event and returns updated state.
@@ -184,7 +199,15 @@ func processStreamEvent(e iteragent.Event, fullContent string, toolStart time.Ti
184199 fmt .Printf ("%s%s %s%s" , col , icon , label , colorReset )
185200 case iteragent .EventToolExecutionEnd :
186201 elapsed := time .Since (toolStart ).Round (time .Millisecond )
187- printToolResult (e .Result , elapsed , e .ToolName , repoPath )
202+ if e .IsError {
203+ fmt .Printf ("%s ✗ %s%s %s%s%s\n " ,
204+ colorDim , colorReset ,
205+ colorRed , e .Result , colorReset ,
206+ colorDim )
207+ fmt .Print (colorReset )
208+ } else {
209+ printToolResult (e .Result , elapsed , e .ToolName , repoPath )
210+ }
188211 newSpinner ("thinking" )
189212 case iteragent .EventContextCompacted :
190213 fmt .Printf ("\r \033 [K%s[context compacted]%s\n " , colorDim , colorReset )
@@ -202,34 +225,38 @@ func printToolResult(result string, elapsed time.Duration, toolName string, repo
202225 }
203226}
204227
205- // updateSessionTokens updates session token counters from the last agent message.
228+ // updateSessionTokens updates session token counters from the last assistant
229+ // message with usage data. Searches backwards since tool result messages (role
230+ // user) may appear after the final assistant message.
206231func updateSessionTokens (a * iteragent.Agent , fullContent string ) {
207- if len (a .Messages ) > 0 {
208- last := a .Messages [len (a .Messages )- 1 ]
209- if last .Usage != nil {
210- sess .InputTokens += last .Usage .InputTokens
211- sess .OutputTokens += last .Usage .OutputTokens
212- sess .CacheRead += last .Usage .CacheRead
213- sess .CacheWrite += last .Usage .CacheWrite
214- sess .Tokens += last .Usage .TotalTokens
232+ for i := len (a .Messages ) - 1 ; i >= 0 ; i -- {
233+ if a .Messages [i ].Usage != nil {
234+ u := a .Messages [i ].Usage
235+ sess .InputTokens += u .InputTokens
236+ sess .OutputTokens += u .OutputTokens
237+ sess .CacheRead += u .CacheRead
238+ sess .CacheWrite += u .CacheWrite
239+ sess .Tokens += u .TotalTokens
240+ return
215241 }
216- } else {
217- approxTokens := len (fullContent ) / 4
218- sess .Tokens += approxTokens
219- sess .OutputTokens += approxTokens
220242 }
243+ // Fallback: approximate from streamed content length.
244+ approxTokens := len (fullContent ) / 4
245+ sess .Tokens += approxTokens
246+ sess .OutputTokens += approxTokens
221247}
222248
223- // printFinalStats prints token delta, status line, and debug log.
224- func printFinalStats (elapsed time.Duration , beforeTokens int , fullContent string ) {
225- fmt . Println ()
226- logTokenDelta ( beforeTokens )
249+ // printFinalStats prints the status line and debug log.
250+ func printFinalStats (elapsed , ttft time.Duration , beforeTokens int , fullContent string ) {
251+ delta := sess . Tokens - beforeTokens
252+
227253 fmt .Println ()
228254 selector .InputTokens = sess .InputTokens
229255 selector .OutputTokens = sess .OutputTokens
230256 selector .SafeMode = cfg .SafeMode
231- selector .PrintStatusLine (elapsed )
257+ selector .TTFT = ttft
258+ selector .PrintStatusLine (elapsed , delta )
232259 fmt .Println ()
233260
234- slog .Debug ("request completed" , "elapsed_ms" , elapsed .Milliseconds (), "response_chars" , len (fullContent ), "total_tokens" , sess .Tokens )
261+ slog .Debug ("request completed" , "elapsed_ms" , elapsed .Milliseconds (), "ttft_ms" , ttft . Milliseconds (), " response_chars" , len (fullContent ), "total_tokens" , sess .Tokens )
235262}
0 commit comments