[BUG] Mid-turn task-summary classifier keeps the previous conversation's `latestAsk` across /clear (and image-bearing prompts never refresh it) — activity line describes a cleared conversation
Environment
- Claude Code 2.1.267
- Windows 11, interactive REPL in the VS Code integrated terminal, Opus 5 (1M context), high effort
- Remote Control enabled — this is what activates the
summary/headlinesurfaces of the mid-turn classifier
Summary
After /clear, the mid-turn task-summary classifier keeps the previous conversation's latestAsk and writes an activity line about it, on top of the new conversation's tool calls. The line is stamped with a fresh at: Date.now(), so the renderer's staleness guard does not suppress it, and the same phrase is mirrored to Remote Control as external_metadata.task_summary — the phone shows the wrong topic too.
Observed, five minutes into a turn that had nothing to do with the previous conversation:
● <a 4-word summary of the previous conversation's last ask>
⎿ $ cd <project> && sed -n '<range>' <path>
The previous conversation had been cleared about three minutes earlier. The current one was unrelated and shared no vocabulary with it, and the sed call is genuinely part of the current work — only the headline is stale.
Prompt text is elided throughout; timestamps, message shapes and code excerpts are unmodified.
Two independent defects combine here. Either one alone is a bug; together they produce a headline that describes a conversation the user already cleared.
Root cause
Symbol names from the shipped binary at ~/.local/share/claude/versions/2.1.267.
1. The classifier job state is process-scoped and no conversation reset clears it
var ICt;
function NU(e){ if(ICt===void 0) ICt = QC?.().createClassifierJobState(e) ?? null; return ICt }
and the second construction site, keyed by the host object, which also outlives a conversation:
class zc{ state = void 0 }
var By = new z(()=>new zc);
function Hc(w){ let I = By.of(w.session.host); if(I.state===void 0) I.state = Di?.().createClassifierJobState(w) ?? null; return I.state }
createClassifierJobState seeds the conversation-scoped fields:
function gn(e={}){ return { ..., prevState:"", prevStateSince:Date.now(), accumulatedOutputs:{},
lastClassifyAt:0, capturedIntent:"", ..., latestAsk:"", kicked:!1, lastBridgeNeeds:void 0,
lastMsgCount:0, ..., lastEmittedDetail:"", lastMidturnLlmAt:0, lastMidturnLlmDetail:"",
midturnLlmIntervalMs:0, midturnLlmEpoch:0, lastResult:null } }
The module exports no reset for it (export{... createClassifierJobState, ..., markApiFailure, markTurnAborted, markTurnActive, ...}); markTurnAborted clears only turn-scoped fields (lastEmittedDetail, lastMidturnLlmDetail, the epoch), never latestAsk, capturedIntent, prevState or lastMsgCount.
clearConversation resets a great deal — tracked file state, task registry, nested memory paths, session env vars, memory selector, file history — and emits conversation_reset, but it never calls into the classifier:
async function*Nln({session:o, setMessages:m, readFileState:M, ...}){
...
if(M.clear(), r5.of(o.host).reset(), v) for(let e of Object.keys(v)) delete v[e];
...
if(Ae()||hc()&&R8()) lc({type:"conversation_reset", new_conversation_id:P}), b("bridge_conversation_reset");
conversation_reset does reach sessionState.notifyConversationReset(), but the only subscriber wired to onConversationReset is the autocompact-state notifier. Nothing resets the classifier state, and taskSummary is absent from the app-state reset table used by /clear:
var r={bagelActive:void 0, ..., activeGoal:void 0, ..., artifactReadVersions:S, artifactReadObservers:l}, _=Object.keys(r);
So after /clear, latestAsk still holds the last ask of the conversation that was just discarded.
2. findLatestRealUserAsk only accepts string content, so an image-bearing prompt never refreshes latestAsk
function Sn(e){
let n = e.findLast(r => r.type==="user" && !r.isMeta
&& typeof r.message.content === "string"
&& !_M(r.message.content) && !GEe(r.message.content, ()=>!0));
return n?.type==="user" && typeof n.message.content==="string" ? n.message.content : void 0
}
function kn(e,n){ if(!n) return; e.latestAsk = fs(Xr(me(n)), 300) }
A prompt with a pasted screenshot has message.content as an array ([{type:"image"},{type:"text"}]), so findLatestRealUserAsk returns undefined, the call site skips captureLatestAsk, and the stale value stands:
let Jc = QC().findLatestRealUserAsk(Ar);
if(Jc) QC().captureLatestAsk(ds, Jc);
captureIntent has the same shape of problem in the other direction — it is write-once for the process (if(e.capturedIntent||!n) return e.capturedIntent), so it also never follows a /clear.
3. Why it reaches the screen
Mid-turn, the ask is often the only real signal — the same mechanism as #87533. Early in a turn there is no assistant prose yet, so the classifier prompt is a tool-name tally plus User's most recent ask: "<previous conversation's ask>", and the model writes the line around that ask:
p = { state:"working", tempo:"active", detail: e.lastMidturnLlmDetail || ot(s, e.describeToolUse), needs:void 0, output:{}, source:"midturn" };
...
let x = await Mt(Ce(ye,{prev:"working", latestAsk:e.latestAsk, toolSummary:_e, minsInState:..., engine:d, surfaces:c, credentials:e.credentials}), $e, "midturn classifier timed out")...
if(x?.source==="llm" && x.detail && e.midturnLlmEpoch===xe) e.lastMidturnLlmDetail = x.detail, p = {...p, detail:x.detail}
The emit site stamps the current time and publishes to both surfaces:
ds.onClassified=(Su,cf)=>{
if(!cf || !(ho||$a) || Su.detail===ds.lastEmittedDetail) return;
if(ds.lastEmittedDetail=Su.detail, ho) Vt.sessionState?.notifyMetadataChanged({task_summary:Su.detail});
if($a){ let Vd = Su.detail ? {text:Su.detail, at:Date.now()} : null;
Vt.setAppState(r_ => r_.taskSummary?.text===Vd?.text ? r_ : {...r_, taskSummary:Vd}) }
}
The TUI's only staleness guard is a timestamp comparison against the message being rendered:
vf = (bb) => Ue && !bb.viewingAgentTaskId && bb.taskSummary!==null && !(bb.taskSummary.at < pN) ? bb.taskSummary.text : null // pN = Date.parse(se.timestamp)
Because at is the emit time, not the time of the content the phrase describes, a phrase derived from a discarded conversation passes the guard on every message of the new one. There is no conversation identity on taskSummary, so nothing else can catch it.
The summary surface is what Remote Control consumes (external_metadata.task_summary, described in the SDK types as "Mid-turn progress line from the debounced classifier"), which is why this is only visible with RC on, and why the wrong topic also reaches the phone.
Repro
- Enable Remote Control.
- Ask any question in a text-only prompt, on a topic with distinctive vocabulary. Let the turn finish.
/clear.- Submit an unrelated prompt with an image attached (pasted screenshot), one that will run for a few minutes of tool calls.
- Watch the
●activity line above the running tool group: it describes step 2's question, not step 4's.
The image in step 4 matters. A text-only prompt refreshes latestAsk on the first classification and masks defect 1; with an image, latestAsk is never overwritten and the previous conversation's ask survives indefinitely.
Evidence from the local transcripts
- previous session
<session A>, last user message with string content,03:21:09Z: a one-sentence question, stringcontent, no attachments — the ask the stale headline summarizes - current session
<session B>:/clearat03:24:39Z; the only real prompt,03:27:20Z, hascontent: [image, text]; the rendered Bash call at03:32:08ZisisSidechain:falseand appears only in this session's JSONL - both sessions carry the same
bridgeSessionId(/clearkeeps the process and the bridge session), so the process-scoped classifier state carried straight across
The headline is a faithful summary of the previous session's last ask, rendered over the current session's work.
Suggested fix
- Reset the classifier job state on conversation reset. The hook already exists —
sessionState.notifyConversationReset()fires on/clear; subscribe the classifier to it and clear at minimumlatestAsk,capturedIntent,prevState,prevStateSince,lastMsgCount,lastResultandaccumulatedOutputs, alongside the turn-scoped fieldsmarkTurnAbortedalready handles. ClearingappState.taskSummaryinclearConversation, and the mirroredtask_summarymetadata, would make the reset complete. - Make
findLatestRealUserAskhandle array content by joining itstextblocks, so multimodal prompts refresh the ask like text prompts do. As written, a conversation whose prompts all carry images never updateslatestAskat all — even without/clear. - Consider carrying the conversation id on
taskSummaryand dropping it on mismatch, so a stale phrase cannot outlive the conversation it came from regardless of timestamps.
Related: #87533 (same classifier: latestAsk hard-truncated at 300 chars with no marker, and the classifier's complaint about that rendered as the activity line).