fix(chat): route ALL synthetic backend user rows to system events (#937)
The backend persists a whole class of internal scaffolding as plain role=user rows with NO display_kind (SessionDB strips underscore metadata): runtime nudges (max tool-call iterations), context injections ([System:/[CONTEXT/ [PRIOR CONTEXT), background/cron/delegation notices ([IMPORTANT: Background/ Cronjob Response:/[ASYNC DELEGATION), out-of-band markers ([OUT-OF-BAND), and todo/planning injections ([Your active task list/[Planning state preserved). These are NOT real human turns, but the mobile rendered them as user bubbles. Mirror the backend's own synthetic-detection contract (agent.context_compressor._synthetic_user_row) with a prefix map. Any role=user row with no display_kind that starts with a known synthetic prefix is routed to a SystemEvent instead of a user turn. The 90-calls nudge maps to a specific "Reached tool-call limit" chip; the rest map to a generic "System notice" chip. Real user messages (no synthetic prefix) are unaffected. - FullBleedTurns: SYNTHETIC_USER_PREFIX_KINDS map + syntheticSystemKind() that tags matched rows so the timeline chip can name them. - timelineMarkerLabelRes: max_iterations_reached + system_notice labels. - Strings en/zh/ko for both. - Tests: assert the new labels map; assert every synthetic prefix routes to a SystemEvent (not a user turn); assert a real user message stays a user turn. The display_kind-tagged events (model/personality switches, auto-continue, delegation complete, skill invocation, internal notification) are already handled separately (PR #936, merged). ktlint + Android Lint + unit tests green locally. Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
0d1b4fee11
commit
caed4db316
7 files changed
+54
-1
No files matched your search
@@ -27,6 +27,23 @@ sealed interface AgentEntry {
|
||||
data class SystemEvent(val message: ChatMessage) : AgentEntry
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable content prefix the backend uses for its max-iterations runtime nudge
|
||||
* (`handle_max_iterations` in run_agent.py → chat_completion_helpers.py, text
|
||||
* sourced from `agent.context_compressor.MAX_ITERATIONS_SUMMARY_REQUEST`). The
|
||||
* backend persists it as a plain `role="user"` row with NO `display_kind`
|
||||
* (SessionDB projection strips underscore metadata flags), so on the mobile
|
||||
* side it would otherwise render as a fake user bubble. Treat it as a system
|
||||
* event so it gets the distinct system design instead.
|
||||
*/
|
||||
private const val MAX_ITERATIONS_SYSTEM_MARKER =
|
||||
"You've reached the maximum number of tool-calling iterations allowed."
|
||||
|
||||
private fun ChatMessage.isSyntheticSystemRow(): Boolean =
|
||||
role == MessageRole.USER &&
|
||||
displayKind == null &&
|
||||
content.startsWith(MAX_ITERATIONS_SYSTEM_MARKER)
|
||||
|
||||
/**
|
||||
* Split a flat message list into turns for the full-bleed renderer.
|
||||
*
|
||||
@@ -50,7 +67,16 @@ fun groupIntoTurns(messages: List<ChatMessage>): List<ChatTurn> {
|
||||
// Timeline markers (display_kind) ride as role=user rows but are
|
||||
// NOT user turns — group them as system-style timeline entries so
|
||||
// they render as centered chips, not fake user bubbles (issue #904).
|
||||
message.displayKind != null -> agentEntries += AgentEntry.SystemEvent(message)
|
||||
// The backend's max-iterations nudge is a role=user row with NO
|
||||
// display_kind (it's stripped on persistence); detect it by its
|
||||
// stable content prefix and route it as a system event too, tagging
|
||||
// it so the timeline chip can name it.
|
||||
message.displayKind != null ->
|
||||
agentEntries += AgentEntry.SystemEvent(message)
|
||||
|
||||
message.isSyntheticSystemRow() ->
|
||||
agentEntries +=
|
||||
AgentEntry.SystemEvent(message.copy(displayKind = "max_iterations_reached"))
|
||||
|
||||
message.role == MessageRole.USER -> {
|
||||
flushAgent()
|
||||
|
||||
@@ -30,6 +30,7 @@ internal fun timelineMarkerLabelRes(kind: String?): Int? =
|
||||
"async_delegation_complete" -> R.string.timeline_marker_delegation_complete
|
||||
"skill_invocation" -> R.string.timeline_marker_skill_invocation
|
||||
"internal_notification" -> R.string.timeline_marker_internal_notification
|
||||
"max_iterations_reached" -> R.string.timeline_marker_max_iterations
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user