MCPcopy Index your code
hub / github.com/codehamr/codehamr / handleStreamClosed

Method handleStreamClosed

internal/tui/model.go:911–972  ·  view source on GitHub ↗

handleStreamClosed drives what happens after one round's stream finishes: dispatch the next pending tool call, or, if none, finalize the turn and hand control back. A turn ends precisely when the assistant emits no tool calls; there is no loop tool to land on.

()

Source from the content-addressed store, hash-verified

909 // finish it's already 0 (applyDone folded the round into turnTokens at
910 // EventDone). But a Ctrl+C or error mid-stream interrupts before EventDone,
911 // so the cancelled round's tokens (often the whole generation) sit only in
912 // streamingEstimate; without this they'd vanish from the avg and the session
913 // total would drop backward. char/4 is the best count for a round that never
914 // reported usage.
915 m.turnTokens += m.streamingEstimate
916 m.sessionTokens += m.streamingEstimate
917 m.streamingEstimate = 0
918 wall := time.Since(m.turnStart)
919 m.lastElapsed = wall
920 m.lastTokens = m.turnTokens
921 m.lastOutcome = outcome
922 avg := humanRate(m.turnTokens, wall)
923 if avg != "" {
924 avg = " · " + avg + " avg"
925 }
926 dbgWritef("turn_end", "%s · %s wall%s · session_total=%s",
927 humanTokens(m.turnTokens), wall.Round(time.Millisecond), avg, humanTokens(m.sessionTokens))
928 m.turnStart = time.Time{}
929 m.turnTokens = 0
930}
931
932// handleStreamClosed drives what happens after one round's stream finishes:
933// dispatch the next pending tool call, or, if none, finalize the turn and
934// hand control back. A turn ends precisely when the assistant emits no tool
935// calls; there is no loop tool to land on.
936func (m Model) handleStreamClosed() (tea.Model, tea.Cmd) {
937 m.stream = nil
938 // Stale close from a cancelled turn (handleCtrlC / EventError reset phase
939 // to idle).
940 if !m.phase.active() {
941 return m, nil
942 }
943 if len(m.pending) > 0 {
944 // The model issued a tool call, genuine progress. Re-arm the empty-reply
945 // latch so a LATER transient empty on this same (long) turn earns its own
946 // re-prompt instead of hitting the leak-and-die branch below. The latch
947 // exists to stop a server that deterministically swallows EVERY call, not
948 // to cap recoveries on a turn that keeps advancing: a flaky stream that
949 // drops the occasional call must not abandon a half-built file (the galaxy1
950 // failure: empty → nudge → recovered with a write → empty again → died).
951 // Two CONSECUTIVE empties still terminate: pending is 0 on that path, so
952 // this never re-arms there.
953 m.emptyNudged = false
954 return m.dispatchNextTool()
955 }
956 // The turn is ending with no tool calls. If the model said nothing and called
957 // nothing, it either stopped mid-task or its tool call was swallowed (a
958 // thinking model streams the call into the reasoning channel, which never
959 // reaches us as content or a structured call). Re-prompt once to re-issue or
960 // finish; the emptyNudged latch bounds it to a single retry so a server that
961 // deterministically swallows the call can't loop. If it persists, surface it
962 // rather than dying silently: the prior behaviour left a half-done artifact
963 // with no banner at all.
964 outcome := outcomeDone // a clean, non-empty finish; stall/leak below downgrade it
965 if newestAssistantEmpty(m.history) {
966 if !m.emptyNudged {
967 m.emptyNudged = true
968 dbgWritef("nudge", "empty-reply nudge injected (turn ended with no content and no tool call)")

Calls 11

dispatchNextToolMethod · 0.95
startChatMethod · 0.95
appendLineMethod · 0.95
maybeVerifyNudgeMethod · 0.95
finalizeTurnMethod · 0.95
endTurnMethod · 0.95
fireQueuedMethod · 0.95
newestAssistantEmptyFunction · 0.85
dbgWritefFunction · 0.85
toolCallLeakWarningFunction · 0.85
activeMethod · 0.80