toolResultFailed reports whether a tool result is an error the model should react to. File tools wrap errors in parens ("(write error: ...)", "(not found: ...)") and report success as plain text ("wrote N bytes"); bash appends "(exit: N)" / "(timeout after ...)" on failure. A user Ctrl+C ("(cancelle
(name, result string)
| 1104 | // call into the reply text instead. The fix is server-side. |
| 1105 | m.appendLine(w) |
| 1106 | dbgWritef("leak", "turn ended with tool-call text leaked into the reply (server-side parser misconfigured)") |
| 1107 | outcome = outcomeStopped |
| 1108 | } else if m.maybeVerifyNudge() { |
| 1109 | // A substantial turn is finishing with a clean, non-empty summary. Re-ground |
| 1110 | // it once to the original request and let the model verify (or honestly mark |
| 1111 | // unverified) before it hands control back. Mirrors the empty-reply re-prompt: |
| 1112 | // applyDone already flushed this summary to scrollback and appended it to |
| 1113 | // history, so the streaming buffer is clean and startChat resumes safely. |
| 1114 | m.phase = phaseThinking |
| 1115 | return m, m.startChat() |
| 1116 | } |
| 1117 | m.finalizeTurn(outcome) |
| 1118 | m.endTurn() |
| 1119 | return m.fireQueued() |
| 1120 | } |
| 1121 | |
| 1122 | // fireQueued auto-submits a prompt the user queued mid-turn, once the turn has |
| 1123 | // wound down to idle. Reached only from the natural finish path (here, after |
| 1124 | // finalizeTurn/endTurn); a Ctrl+C or stream-error abort routes through abortTurn |
| 1125 | // and never gets here, so an interrupt leaves the slot for a manual send. No-op |
| 1126 | // when nothing is queued. The expanded send goes to the LLM, the collapsed echo |
| 1127 | // to scrollback, exactly as a typed submit; the recall entry carries the expanded |
| 1128 | // text (no chip), matching disk-loaded history. |
| 1129 | func (m Model) fireQueued() (tea.Model, tea.Cmd) { |
| 1130 | if m.queued == nil { |
| 1131 | return m, nil |
| 1132 | } |
| 1133 | q := m.queued |
| 1134 | m.queued = nil |
| 1135 | return m.submit(q.send, q.echo, promptEntry{display: q.send}) |
| 1136 | } |
| 1137 | |
| 1138 | // newestAssistant returns the newest assistant-role message in history: the |
| 1139 | // turn's final reply, which all three finish checks below inspect. |
| 1140 | func newestAssistant(history []chmctx.Message) (chmctx.Message, bool) { |
| 1141 | for i := len(history) - 1; i >= 0; i-- { |
| 1142 | if history[i].Role == chmctx.RoleAssistant { |
| 1143 | return history[i], true |
no outgoing calls