Skip to content

Commit e1a082b

Browse files
committed
chore: bump async-context-compression to v1.7.3
Bumps version 1.7.2 → 1.7.3 and ships three fixes plus end-to-end verification: - Summary persistence on fresh PostgreSQL: dedup stale unique-index definitions in shared SQLAlchemy metadata before CREATE TABLE, and idempotently clear legacy colliding indexes. Resolves DuplicateTable: relation "ix_chat_summary_chat_id" already exists and the resulting ⚠️ Summary generated but was not persisted. - Outlet summary reuse for idless plain-chat branches: when the outlet request carries a chat_id but no stable message refs, read the active DB branch and align the body via _compatible_db_branch_for_body_ref_fallback so the generated summary can be persisted and reused. - Reasoning-model inlet reuse (issue #98): new position-based fallback (Path 3) accepts the snapshot when body and DB branches have equal length and roles / tool_calls / tool_call_id match position-by-position. DB messages with an output array are exempted from content comparison; DB messages without output still require exact content equality (fail-closed against edits). - Path 3 mixed-id fix: process_messages_with_output only strips output (not id), so real reasoning-chat bodies are mixed-id. The all-idless guard was removed — reaching Path 3 already requires _current_branch_refs(messages) is None upstream, so the guard was both wrong and redundant. - Path 3 diagnostic logging: when Path 3 is eligible but a per-position check fails, debug_mode now logs the first failing index and mismatched field (role / tool_calls / tool_call_id / content-on-no-output). - End-to-end verification: test_issue98_e2e.py inlines convert_output_to_messages, process_messages_with_output, and reconcile_tool_pairs copied verbatim from the OpenWebUI main branch. The body builder mirrors the real pipeline exactly (genuinely mixed-id), replaying OpenAI-compatible / Ollama / llama.cpp / tool-call reasoning chats through the full inlet() entry point. 15/15 e2e tests pass. Also adds v1.7.3 release notes (EN + CN), updates README/README_CN, and removes the temporary _diag.py diagnostic script.
1 parent 1416abb commit e1a082b

7 files changed

Lines changed: 183 additions & 97 deletions

File tree

plugins/filters/async-context-compression/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Async Context Compression Filter
22

3-
| By [Fu-Jie](https://github.com/Fu-Jie) · v1.7.2 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) |
3+
| By [Fu-Jie](https://github.com/Fu-Jie) · v1.7.3 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) |
44
| :--- | ---: |
55

66
| ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) |
@@ -28,6 +28,15 @@ When the selection dialog opens, search for this plugin, check it, and continue.
2828
- **Protected-head tracking**: Summary rows remember how many leading messages were kept outside the summary. If the current `keep_first` policy no longer preserves those messages, the row is not reused as branch-valid coverage.
2929
- **Safe upgrade behavior**: Legacy summaries without coverage metadata are not trusted as coverage. The first turn after upgrading may send more raw context until a branch-valid summary row is generated.
3030

31+
## What's new in 1.7.3
32+
33+
- **Summary persistence on fresh PostgreSQL**: Fixed `DuplicateTable: relation "ix_chat_summary_chat_id" already exists` and the resulting `⚠️ Summary generated but was not persisted`. The shared SQLAlchemy metadata is now deduplicated before `CREATE TABLE`, and legacy colliding indexes are cleared idempotently.
34+
- **Outlet summary reuse for idless plain-chat branches**: When the outlet request carries a `chat_id` but no stable message refs, the filter now reads the active DB branch and aligns the body against it so the generated summary can be persisted and reused on subsequent turns.
35+
- **Reasoning-model inlet reuse (issue #98)**: Reasoning models store folded `<details type="reasoning">` content in the DB, but the request body reconstructed by `process_messages_with_output` strips or re-tags the reasoning, so cached summaries were rejected every turn. A new position-based fallback (Path 3) accepts the snapshot when body and DB branches have equal length and roles / tool_calls / tool_call_id match position-by-position. DB messages with an `output` array are exempted from content comparison; DB messages without `output` still require exact content equality, so edited or tampered bodies are rejected.
36+
- **Path 3 mixed-id fix**: `process_messages_with_output` only strips `output` (not `id`), so real reasoning-chat bodies are mixed-id — the all-idless guard was removed; Path 3 now accepts real reasoning chats.
37+
- **Path 3 diagnostic logging**: when Path 3 is eligible but a per-position check fails, `debug_mode` now logs the first failing index and the mismatched field (role / tool_calls / tool_call_id / content), so silent rejections are observable.
38+
- **End-to-end verification**: A new test module inlines `convert_output_to_messages`, `process_messages_with_output`, and `reconcile_tool_pairs` copied verbatim from the OpenWebUI main branch, and replays OpenAI-compatible / Ollama / llama.cpp / tool-call reasoning chats through the full `inlet()` entry point. The body builder mirrors the real pipeline exactly (genuinely mixed-id), with regression tests covering the mixed-id shape and Path 3 acceptance.
39+
3140
## What's new in 1.7.2
3241

3342
- **Summary injection safety guard**: Injected summaries now explicitly state that goals, open loops, and tool state inside the summary are historical context only, not new instructions.
@@ -221,6 +230,6 @@ If this plugin has been useful, a star on [OpenWebUI Extensions](https://github.
221230

222231
## Changelog
223232

224-
See [`v1.7.2` Release Notes](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/v1.7.2.md) for the release-specific summary.
233+
See [`v1.7.3` Release Notes](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/v1.7.3.md) for the release-specific summary.
225234

226235
See the full history on GitHub: [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)

plugins/filters/async-context-compression/README_CN.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 异步上下文压缩过滤器
22

3-
| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v1.7.2 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) |
3+
| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v1.7.3 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) |
44
| :--- | ---: |
55

66
| ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) |
@@ -30,6 +30,15 @@
3030
- **受保护头部追踪**:摘要行会记录有多少开头消息是在摘要之外按原文保留的。如果当前 `keep_first` 策略已经不再保留这些消息,该摘要行不会作为 branch-valid 覆盖范围复用。
3131
- **安全升级行为**:没有覆盖范围元数据的 legacy summary 不再被当成可信覆盖。升级后的第一轮对话可能会发送更多原始上下文,直到生成 branch-valid 摘要行。
3232

33+
## 1.7.3 版本更新
34+
35+
- **新 PostgreSQL 上的 summary 持久化**:修复了 `DuplicateTable: relation "ix_chat_summary_chat_id" already exists` 以及随之出现的 `⚠️ Summary generated but was not persisted``CREATE TABLE` 之前会先对共享的 SQLAlchemy metadata 做索引去重,并幂等清理撞名的 legacy 索引。
36+
- **无 id 普通对话分支的 outlet summary 复用**:当 outlet 请求带 `chat_id` 但没有稳定 message refs 时,插件现在会读取数据库里的 active branch 并对齐 body,使生成的 summary 可以持久化并在后续轮次复用。
37+
- **Reasoning model 的 inlet 复用(issue #98**:Reasoning model 在数据库里保存带折叠 `<details type="reasoning">` 的 content,但 `process_messages_with_output` 重建请求 body 时会剥离或改写 reasoning,导致缓存的 summary 每轮都被拒绝。新增的 position-based 兜底路径(Path 3)在 body 与 DB 分支长度相同、role / tool_calls / tool_call_id 按位置匹配时接受 snapshot。带 `output` 数组的 DB 消息豁免 content 比对;没有 `output` 的 DB 消息仍要求 content 精确匹配,因此被编辑或篡改的 body 会被拒绝。
38+
- **Path 3 混合 id 修复**`process_messages_with_output` 只剥离 `output`(不剥离 `id`),真实 reasoning 对话的 body 是混合 id 的——all-idless 守卫已移除,Path 3 现在能接受真实 reasoning 对话。
39+
- **Path 3 诊断日志**:当 Path 3 满足条件但某个位置检查失败时,`debug_mode` 现在会记录第一个失败的位置索引和不匹配的字段(role / tool_calls / tool_call_id / content),让静默拒绝变得可见。
40+
- **端到端验证**:新增测试模块内联了从 OpenWebUI main 分支逐行复制的 `convert_output_to_messages``process_messages_with_output``reconcile_tool_pairs`,并对 OpenAI 兼容 / Ollama / llama.cpp / 带 tool_calls 的 reasoning 对话走完整 `inlet()` 入口进行回放验证。body 构造精确镜像真实管道(真正的混合 id),回归测试覆盖混合 id 形状和 Path 3 接受。
41+
3342
## 1.7.2 版本更新
3443

3544
- **摘要注入安全边界**:注入给模型的 summary 现在会明确说明,summary 里的目标、待办和工具状态只代表历史上下文,不是新的指令。
@@ -262,6 +271,6 @@ flowchart TD
262271

263272
## 更新日志
264273

265-
请查看 [`v1.7.2` 版本发布说明](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/v1.7.2_CN.md) 获取本次版本的独立发布摘要。
274+
请查看 [`v1.7.3` 版本发布说明](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/filters/async-context-compression/v1.7.3_CN.md) 获取本次版本的独立发布摘要。
266275

267276
完整历史请查看 GitHub 项目: [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)

plugins/filters/async-context-compression/_diag.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

plugins/filters/async-context-compression/async_context_compression.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
author_url: https://github.com/Fu-Jie/openwebui-extensions
66
funding_url: https://github.com/open-webui
77
description: Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.
8-
version: 1.7.2
8+
version: 1.7.3
99
openwebui_id: b1655bc8-6de9-4cad-8cb5-a6f7829a02ce
1010
license: MIT
1111
@@ -1157,6 +1157,47 @@ def _body_position_matches_db_message(
11571157

11581158
return True
11591159

1160+
def _first_body_position_mismatch(
1161+
self,
1162+
body_messages: List[Dict[str, Any]],
1163+
db_messages: List[Dict[str, Any]],
1164+
) -> Optional[tuple]:
1165+
"""Return (index, reason) of the first position mismatch, or None.
1166+
1167+
Mirrors :meth:`_body_position_matches_db_message` but returns a
1168+
human-readable reason instead of a boolean, so the inlet can log
1169+
exactly which position and field caused Path 3 to reject the body.
1170+
"""
1171+
for index, (body_message, db_message) in enumerate(
1172+
zip(body_messages, db_messages)
1173+
):
1174+
if self._normalize_role(body_message.get("role")) != self._normalize_role(
1175+
db_message.get("role")
1176+
):
1177+
return index, (
1178+
f"role mismatch (body={body_message.get('role')!r}, "
1179+
f"db={db_message.get('role')!r})"
1180+
)
1181+
if body_message.get("tool_calls") != db_message.get("tool_calls"):
1182+
return index, "tool_calls mismatch"
1183+
if body_message.get("tool_call_id") != db_message.get("tool_call_id"):
1184+
return index, (
1185+
f"tool_call_id mismatch "
1186+
f"(body={body_message.get('tool_call_id')!r}, "
1187+
f"db={db_message.get('tool_call_id')!r})"
1188+
)
1189+
db_output = db_message.get("output")
1190+
has_db_output = isinstance(db_output, list) and bool(db_output)
1191+
if not has_db_output:
1192+
if body_message.get("content") != db_message.get("content"):
1193+
body_preview = str(body_message.get("content"))[:120]
1194+
db_preview = str(db_message.get("content"))[:120]
1195+
return index, (
1196+
f"content mismatch on no-output message "
1197+
f"(body={body_preview!r}, db={db_preview!r})"
1198+
)
1199+
return None
1200+
11601201
def _message_fingerprint(self, message: Dict[str, Any]) -> str:
11611202
"""Fingerprint the model-visible payload to detect in-place edits."""
11621203
payload = self._message_fingerprint_payload(message)
@@ -2503,9 +2544,9 @@ def _body_to_db_coverage_map_for_ref_fallback(
25032544
):
25042545
return db_to_body_boundaries
25052546

2506-
# Path 3 (position-based fallback): when the body carries no
2507-
# OpenWebUI message ids and matches the DB active branch 1:1 in
2508-
# count and role sequence, accept the DB refs by position.
2547+
# Path 3 (position-based fallback): when the body matches the DB
2548+
# active branch 1:1 in count and role / tool_calls / tool_call_id
2549+
# sequence, accept the DB refs by position.
25092550
#
25102551
# This covers reasoning models (and any future rebuild path) where
25112552
# OpenWebUI regenerates assistant ``content`` from the ``output``
@@ -2515,6 +2556,15 @@ def _body_to_db_coverage_map_for_ref_fallback(
25152556
# ``<details type="reasoning">`` block in the DB but stripped from
25162557
# the body). Content-level comparison cannot succeed in that case.
25172558
#
2559+
# The body need NOT be fully idless. ``process_messages_with_output``
2560+
# only strips ``output`` (not ``id``), so user / system / no-output
2561+
# assistant messages keep their DB node ``id`` while only the
2562+
# rebuilt assistant-with-output messages become idless — the request
2563+
# body is mixed-id in practice. We reach this fallback only when
2564+
# ``_current_branch_refs(messages) is None`` upstream (i.e. the body
2565+
# as a whole does not expose a usable ref sequence), so requiring
2566+
# all-idless here would wrongly reject every real reasoning chat.
2567+
#
25182568
# Guards against false positives:
25192569
# - ``unfolded_messages`` non-empty rules out conversion failures
25202570
# (when ``_unfold_db_branch_for_body_ref_fallback`` cannot parse
@@ -2526,7 +2576,6 @@ def _body_to_db_coverage_map_for_ref_fallback(
25262576
# ``tool_call_id`` to match (catches tampered tool calls).
25272577
if (
25282578
unfolded_messages
2529-
and all(not self._get_message_id(m) for m in body_messages)
25302579
and len(body_messages) == len(db_messages)
25312580
and all(
25322581
self._body_position_matches_db_message(body_message, db_message)
@@ -2535,6 +2584,21 @@ def _body_to_db_coverage_map_for_ref_fallback(
25352584
):
25362585
return list(range(len(db_messages) + 1))
25372586

2587+
if (
2588+
unfolded_messages
2589+
and len(body_messages) == len(db_messages)
2590+
and self.valves.debug_mode
2591+
):
2592+
first_mismatch = self._first_body_position_mismatch(
2593+
body_messages, db_messages
2594+
)
2595+
if first_mismatch is not None:
2596+
mismatch_index, mismatch_reason = first_mismatch
2597+
logger.info(
2598+
"[Summary Snapshot] Path 3 position fallback rejected at "
2599+
f"index={mismatch_index}: {mismatch_reason}"
2600+
)
2601+
25382602
return None
25392603

25402604
def _compatible_db_branch_for_body_ref_fallback(

0 commit comments

Comments
 (0)