fix(coding-agent/session): cleared todo reminder and track assistant message synchronously

- Moved `#todoReminderAwaitingProgress` clearing into tool result handler for synchronous state update.
- Moved `#lastAssistantMessage` tracking to message_end to prevent stale reads when events land in same tick.
This commit is contained in:
can1357
2026-07-18 18:56:32 +02:00
parent c439b1eacd
commit 37456c5f4b
2 changed files with 15 additions and 8 deletions
+2 -2
View File
@@ -1,13 +1,13 @@
# Changelog
## [Unreleased]
### Fixed
- Browser tool selectors now accept bare snapshot refs (`tab.click("e501")`, `@e501`) everywhere `aria-ref=e501` works — previously the tab-worker backend fell through to a CSS tag selector that could never match, burning the 2s zero-match watchdog with a misleading "matches no elements" hint. `tab.select`, `tab.uploadFile`, `tab.press({ selector })`, `tab.screenshot({ selector })`, and `tab.drag` now resolve refs too. Unknown/stale refs fail immediately with the "refresh refs" error.
- `tab.select` no longer double-reports the previously selected option of a single `<select>`: the returned selection is read back after the full assignment pass instead of mid-loop.
- Fixed transcript blocks being visibly duplicated during streaming (whole tool boxes and assistant paragraphs recommitted below their first copy on the terminal tape) by removing transcript committed-prefix compaction entirely. Dropping committed rows from the transcript's local frame shifted the frame under the engine's committed-prefix ledger, so the audit re-anchored and recommitted rows the tape already held. The transcript now always keeps its full local frame; committed finalized blocks still skip `render()` via the segment reuse bypass. Reverts the compaction half of [#5930](https://github.com/can1357/oh-my-pi/issues/5930)'s fix (compose keeps the render bypass; the local frame is no longer truncated).
- Fixed classifier refusals (e.g. Anthropic `stop_reason: "refusal"`) ending the turn with no visible error. Two independent regressions: (1) session events reached subscribers out of order when a turn's provider events landed in one tick — extension emits only await for event types with registered handlers, so the assistant `message_end` overtook its own `message_start` and the TUI skipped the error render entirely (no pinned banner, no inline `Error:` line); subscriber fan-out is now serialized in emission order. (2) Refusal turns are pruned from active context at settle (#3591), which also erased them from `state.messages` before `prompt()` resolved — print mode printed nothing and exited 0, and the task executor's `getLastAssistantMessage()` saw the previous turn. The pruned refusal is now retained until the next run starts, `getLastAssistantMessage()` reports it, and print mode reads the settled assistant via that accessor (exit 1 + refusal message on stderr).
- Fixed classifier refusals (e.g. Anthropic `stop_reason: "refusal"`) ending the turn with no visible error. Two independent regressions: (1) session events reached subscribers out of order when a turn's provider events landed in one tick — extension emits only await for event types with registered handlers, so the assistant `message_end` overtook its own `message_start` and the TUI skipped the error render entirely (no pinned banner, no inline `Error:` line); subscriber fan-out is now serialized in emission order. (2) Refusal turns are pruned from active context at settle (#3591), which also erased them from `state.messages` before `prompt()` resolved — print mode printed nothing and exited 0, and the task executor's `getLastAssistantMessage()` saw the previous turn. The pruned refusal is now retained until the next run starts, `getLastAssistantMessage()` reports it, and print mode reads the settled assistant via that accessor (exit 1 + refusal message on stderr). Additionally, `#lastAssistantMessage` is now set synchronously on `message_end` to prevent `agent_end` maintenance from reading a stale assistant turn when tool results and stops land in the same tick.
## [17.0.4] - 2026-07-18
@@ -4497,6 +4497,19 @@ export class AgentSession {
} else if (!isError && MID_RUN_TODO_NUDGE_MUTATING_TOOLS[toolName]) {
this.#mutationsSinceLastTodoTouch++;
}
// A tool actually ran. Clear the post-reminder suppression synchronously
// too: the settle check (`#checkTodoCompletion` in agent_end maintenance)
// can otherwise read the stale flag when a tool result and the terminal
// stop land in the same tick, swallowing the earned re-escalation.
this.#todoReminderAwaitingProgress = false;
}
// Track the settled assistant turn synchronously as well: agent_end
// maintenance reads `#lastAssistantMessage`, and when a turn's events all
// land in one tick its handler can run before this handler's post-emit
// bookkeeping — leaving maintenance looking at the previous (e.g.
// toolUse) assistant message and skipping settle-only work.
if (event.type === "message_end" && event.message.role === "assistant") {
this.#lastAssistantMessage = event.message;
}
// Plan-mode internal transition: stamp `SILENT_ABORT_MARKER` on the
// persisted message BEFORE the obfuscator's display-side copy below.
@@ -4703,9 +4716,7 @@ export class AgentSession {
}
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
// Track assistant message for auto-compaction (checked on agent_end)
if (event.message.role === "assistant") {
this.#lastAssistantMessage = event.message;
const assistantMsg = event.message as AssistantMessage;
// Fold this turn's timing into per-model perf aggregates (drives the
// /models TPS/TTFT display). Errored turns measure nothing; aborted
@@ -4775,10 +4786,6 @@ export class AgentSession {
const details = isRecord(event.message.details) ? event.message.details : undefined;
const semanticResult = semanticToolResult(toolName, event.message);
const semanticDetails = isRecord(semanticResult?.details) ? semanticResult.details : undefined;
// A tool actually ran. Clear the post-reminder suppression: the agent did
// productive work in response to the prior nudge, so the next text-only stop
// is allowed to escalate to the next reminder if todos remain incomplete.
this.#todoReminderAwaitingProgress = false;
// Invalidate streaming edit cache when edit tool completes to prevent stale data
const editedPath = details ? getStringProperty(details, "path") : undefined;
if (toolName === "edit" && editedPath) {