* fix(chat): stop duplicating interim commentary before tool calls (issue #842) Interim commentary the agent narrates before each tool call rendered TWICE. Two mechanisms, both now covered: 1. The old tool.start orphan seal + message.complete's full text: when the complete payload repeats the commentary, the final bubble duplicated it. onMessageComplete now strips the sealed-orphan prefix (tracked in StreamingState) from the final text. 2. On-device capture + DB forensics (2026-08-08) exposed the real repeat offender: the app seals the RAW streamed text — which carries leading blank lines the model emits before its narration — while the backend persists a CLEANED copy (leading whitespace stripped). A transcript reload ~10s later merged the REST page, exact-content matching failed, and the cleaned REST copy rendered on top of the live bubble. sameLogicalMessage now compares trimmed content, so the sealed live bubble is covered by the REST row. Also: - session.interrupt seals the in-flight streaming message before clearing streaming state, so interrupting a commentary turn keeps the partial text - regression tests cover both turn shapes (commentary in the final row → stripped; absent → kept) and the whitespace-drift reload case * fix(chat): match tool rows by gateway call id, drop reasoning-echo dupes (#842) REST transcript rows carry tool_call_id; live WS bubbles keep tool_id from tool.start. Match tool rows on that 1:1 identity so MCP/web rows (stored as raw <untrusted_tool_result> text with no JSON key) dedupe during the post-turn transcript merge instead of ghosting after the final message. reasoning.available echo (narration text == message body, sent ~40ms before each tool.start) no longer overwrites the real reasoning trace, so narration bubbles don't render their own text twice (body + Reasoning card). Room v5 migration adds tool_call_id; regression tests use captured on-device payload shapes. * fix(chat): flush throttled tokens before orphan seal + prefix-safe dedupe (#842) The tool.start seal raced the 33ms throttled token flush: deltas landing just before the transition stayed in the buffer, so the sealed narration orphan came out truncated (on-device capture: orphan ended '...chickpea', backend persisted '...chickpea goodness:') and failed the logical-match against the clean REST row — the commentary ghosted on the next transcript sync. - ChatStreamingController.flushPendingTokens(): force-flushes the throttled TOKEN buffer on MessageStart/MessageComplete/MessageDone/ToolStart so the reducer seals the COMPLETE narration. - sameLogicalMessage: accept prefix-covering (both sides >=40 chars) so a residual seal drift can never re-add a substantial narration. - Regression tests: controller flush + truncated-orphan prefix match. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
95 lines
2.8 KiB
JSON
95 lines
2.8 KiB
JSON
{
|
|
"formatVersion": 1,
|
|
"database": {
|
|
"version": 5,
|
|
"identityHash": "906ca45ef187dc6c219f94803925d20a",
|
|
"entities": [
|
|
{
|
|
"tableName": "chat_messages",
|
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `session_id` TEXT NOT NULL, `role` TEXT NOT NULL, `content` TEXT NOT NULL, `reasoning_text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `tool_name` TEXT, `tool_call_id` TEXT NOT NULL, `tool_status` TEXT, `is_streaming` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
|
"fields": [
|
|
{
|
|
"fieldPath": "id",
|
|
"columnName": "id",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "sessionId",
|
|
"columnName": "session_id",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "role",
|
|
"columnName": "role",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "content",
|
|
"columnName": "content",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "reasoningText",
|
|
"columnName": "reasoning_text",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "timestamp",
|
|
"columnName": "timestamp",
|
|
"affinity": "INTEGER",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "toolName",
|
|
"columnName": "tool_name",
|
|
"affinity": "TEXT"
|
|
},
|
|
{
|
|
"fieldPath": "toolCallId",
|
|
"columnName": "tool_call_id",
|
|
"affinity": "TEXT",
|
|
"notNull": true
|
|
},
|
|
{
|
|
"fieldPath": "toolStatus",
|
|
"columnName": "tool_status",
|
|
"affinity": "TEXT"
|
|
},
|
|
{
|
|
"fieldPath": "isStreaming",
|
|
"columnName": "is_streaming",
|
|
"affinity": "INTEGER",
|
|
"notNull": true
|
|
}
|
|
],
|
|
"primaryKey": {
|
|
"autoGenerate": false,
|
|
"columnNames": [
|
|
"id"
|
|
]
|
|
},
|
|
"indices": [
|
|
{
|
|
"name": "index_chat_messages_session_id_timestamp",
|
|
"unique": false,
|
|
"columnNames": [
|
|
"session_id",
|
|
"timestamp"
|
|
],
|
|
"orders": [],
|
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_chat_messages_session_id_timestamp` ON `${TABLE_NAME}` (`session_id`, `timestamp`)"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"setupQueries": [
|
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '906ca45ef187dc6c219f94803925d20a')"
|
|
]
|
|
}
|
|
} |