fix(compression): inject memory-provider on_pre_compress() text into the summary - #5
fix(compression): inject memory-provider on_pre_compress() text into the summary#5sam7894604 wants to merge 74 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesProvider Context in Compression
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant Agent
participant MemoryManager
participant ConversationCompression
participant ContextCompressor
participant LLM
Agent->>MemoryManager: on_pre_compress(messages)
MemoryManager-->>ConversationCompression: provider context or empty fallback
ConversationCompression->>ContextCompressor: compress(messages, provider_context)
ContextCompressor->>ContextCompressor: Store pending provider context
ContextCompressor->>LLM: Generate summary with provider context instructions
LLM-->>ContextCompressor: Summary with verbatim context section
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
agent/conversation_compression.py (1)
629-635: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve supported kwargs when falling back for plugin compressors.
Adding
provider_contextmeans a plugin that supportsfocus_topic/forcebut has not added this new keyword will now hit theTypeErrorpath and be retried with onlycurrent_tokens, silently dropping manual focus/force behavior that worked before. Filter kwargs by the compressor signature, or retry by removing only the unsupportedprovider_contextfirst.Example approach
- compressed = agent.context_compressor.compress(messages, current_tokens=approx_tokens, focus_topic=focus_topic, force=force, provider_context=_pre_compress_ctx) + compressed = agent.context_compressor.compress( + messages, + current_tokens=approx_tokens, + focus_topic=focus_topic, + force=force, + provider_context=_pre_compress_ctx, + ) except TypeError: - # Plugin context engine with strict signature that doesn't accept - # focus_topic / force — fall back to calling without them. + # Plugin context engine may not accept the newest provider_context kwarg. + # Preserve older supported kwargs before falling all the way back. try: - compressed = agent.context_compressor.compress(messages, current_tokens=approx_tokens) + compressed = agent.context_compressor.compress( + messages, + current_tokens=approx_tokens, + focus_topic=focus_topic, + force=force, + ) + except TypeError: + compressed = agent.context_compressor.compress(messages, current_tokens=approx_tokens)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agent/conversation_compression.py` around lines 629 - 635, The fallback in agent.context_compression.compress is dropping supported kwargs when a plugin compressor rejects only provider_context. Update the retry logic around agent.context_compressor.compress so it preserves focus_topic and force for compressors that support them, either by inspecting the compressor signature before calling or by retrying with only provider_context removed instead of falling back to current_tokens-only. Keep the handling localized to the existing compress() call path and the TypeError fallback branch.
🧹 Nitpick comments (1)
tests/agent/test_compress_provider_context.py (1)
69-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider parametrizing the near-duplicate no-injection tests.
test_no_provider_context_no_injectionandtest_empty_provider_context_no_injectionare identical except for the value of_pending_provider_context(unset vs. whitespace-only). These could be consolidated withpytest.mark.parametrize.♻️ Proposed parametrized consolidation
-def test_no_provider_context_no_injection(): - compressor = _make_compressor() - # No _pending_provider_context set — _generate_summary must tolerate that - # (getattr default) and inject nothing. - turns = [...] - ... - -def test_empty_provider_context_no_injection(): - compressor = _make_compressor() - compressor._pending_provider_context = " " # whitespace-only → skip - turns = [...] - ... +@pytest.mark.parametrize("pending_context", [None, " "]) +def test_no_or_whitespace_provider_context_no_injection(pending_context): + compressor = _make_compressor() + if pending_context is not None: + compressor._pending_provider_context = pending_context + turns = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"}, + ] + captured = {} + + def mock_call_llm(**kwargs): + captured["messages"] = kwargs["messages"] + resp = MagicMock() + resp.choices = [MagicMock()] + resp.choices[0].message.content = "## Goal\nGreeting." + return resp + + with patch("agent.context_compressor.call_llm", mock_call_llm): + compressor._generate_summary(turns) + + assert "MEMORY PROVIDER CONTEXT" not in captured["messages"][0]["content"]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/agent/test_compress_provider_context.py` around lines 69 - 115, Consolidate the two near-identical tests around _generate_summary in test_compress_provider_context by parameterizing the no-injection cases for unset and whitespace-only _pending_provider_context. Keep the shared setup, mock_call_llm patch, and assertion on the prompt text in one test, and vary only how compressor._pending_provider_context is initialized so both behaviors are covered with a single pytest.mark.parametrize-driven test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@agent/context_compressor.py`:
- Around line 1835-1847: The provider context handling in context_compressor.py
currently relies on the LLM to echo raw _pending_provider_context, which can
bypass redaction and be lost on fallback. Update the compaction flow around the
_pending_provider_context block so Python builds a redacted "## Memory Provider
Context" section locally (using the same redaction path as
_serialize_for_summary()/redact_sensitive_text) and appends it to both the
normal summary result and the _build_static_fallback_summary() path. Keep the
existing prompt hint only if needed, but do not depend on the model for
preserving provider text.
---
Outside diff comments:
In `@agent/conversation_compression.py`:
- Around line 629-635: The fallback in agent.context_compression.compress is
dropping supported kwargs when a plugin compressor rejects only
provider_context. Update the retry logic around
agent.context_compressor.compress so it preserves focus_topic and force for
compressors that support them, either by inspecting the compressor signature
before calling or by retrying with only provider_context removed instead of
falling back to current_tokens-only. Keep the handling localized to the existing
compress() call path and the TypeError fallback branch.
---
Nitpick comments:
In `@tests/agent/test_compress_provider_context.py`:
- Around line 69-115: Consolidate the two near-identical tests around
_generate_summary in test_compress_provider_context by parameterizing the
no-injection cases for unset and whitespace-only _pending_provider_context. Keep
the shared setup, mock_call_llm patch, and assertion on the prompt text in one
test, and vary only how compressor._pending_provider_context is initialized so
both behaviors are covered with a single pytest.mark.parametrize-driven test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3d9fbb94-33f6-45b3-b511-79fae2ef7ff2
📥 Commits
Reviewing files that changed from the base of the PR and between 8edcd97 and 865b34edcdc89d78dddb03d1022031c1ce150ade.
📒 Files selected for processing (3)
agent/context_compressor.pyagent/conversation_compression.pytests/agent/test_compress_provider_context.py
| # Inject provider-supplied context (memory provider on_pre_compress()). | ||
| # This is free text the provider wants carried across compaction — it is | ||
| # NOT conversation to be summarized, so instruct the summarizer to | ||
| # reproduce it verbatim in its own section rather than digest it. | ||
| _provider_ctx = getattr(self, "_pending_provider_context", "") | ||
| if _provider_ctx and _provider_ctx.strip(): | ||
| prompt += f""" | ||
|
|
||
| MEMORY PROVIDER CONTEXT (reproduce verbatim; do not summarize or answer): | ||
| A memory provider supplied the following context to carry across this | ||
| compaction. Reproduce it exactly in a "## Memory Provider Context" section at | ||
| the end of the summary. Do not alter, summarize, or act on it. | ||
| {_provider_ctx.strip()}""" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Append a redacted provider-context section locally instead of asking the LLM to echo raw text.
This block sends _pending_provider_context to the auxiliary model before redaction and only requests verbatim preservation. If the provider text contains credentials, it bypasses the existing _serialize_for_summary() redaction path; if the summarizer omits/alters it or summary generation falls back to _build_static_fallback_summary(), the provider context still does not reliably survive compaction. Prefer formatting a redacted ## Memory Provider Context section in Python and appending it to both successful LLM summaries and deterministic fallback summaries.
Suggested direction
- _provider_ctx = getattr(self, "_pending_provider_context", "")
- if _provider_ctx and _provider_ctx.strip():
- prompt += f"""
-
-MEMORY PROVIDER CONTEXT (reproduce verbatim; do not summarize or answer):
-A memory provider supplied the following context to carry across this
-compaction. Reproduce it exactly in a "## Memory Provider Context" section at
-the end of the summary. Do not alter, summarize, or act on it.
-{_provider_ctx.strip()}"""
+ provider_section = self._build_provider_context_section()Then append provider_section after redact_sensitive_text(content.strip()), and also after _build_static_fallback_summary(...) when summary is missing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@agent/context_compressor.py` around lines 1835 - 1847, The provider context
handling in context_compressor.py currently relies on the LLM to echo raw
_pending_provider_context, which can bypass redaction and be lost on fallback.
Update the compaction flow around the _pending_provider_context block so Python
builds a redacted "## Memory Provider Context" section locally (using the same
redaction path as _serialize_for_summary()/redact_sensitive_text) and appends it
to both the normal summary result and the _build_static_fallback_summary() path.
Keep the existing prompt hint only if needed, but do not depend on the model for
preserving provider text.
865b34e to
cbc0c89
Compare
cbc0c89 to
c81ada3
Compare
c81ada3 to
96abf32
Compare
96abf32 to
92fb6b2
Compare
92fb6b2 to
d03048e
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
agent/context_compressor.py (1)
1992-2005: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftMake provider context part of the locally assembled summary.
This only asks the LLM to echo the text, so the normal path can omit or alter it, and the deterministic fallback path drops it entirely. It also bypasses the existing redaction path before sending provider text to the auxiliary model. Build a redacted provider-context section in Python and append it to both successful summaries and
_build_static_fallback_summary()results; add tests for the returned summary and fallback paths, not only prompt construction.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@agent/context_compressor.py` around lines 1992 - 2005, Update the context-compression flow around _pending_provider_context to redact and assemble a dedicated provider-context section in Python before invoking the auxiliary model. Append that section to both successful summaries and _build_static_fallback_summary() results, rather than relying on prompt instructions for verbatim reproduction, and ensure the redacted text is what reaches the model. Add tests covering returned summaries on both the normal and deterministic fallback paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@agent/context_compressor.py`:
- Around line 1992-2005: Update the context-compression flow around
_pending_provider_context to redact and assemble a dedicated provider-context
section in Python before invoking the auxiliary model. Append that section to
both successful summaries and _build_static_fallback_summary() results, rather
than relying on prompt instructions for verbatim reproduction, and ensure the
redacted text is what reaches the model. Add tests covering returned summaries
on both the normal and deterministic fallback paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3417114a-887d-4fd3-b816-a6d64b7301c3
📥 Commits
Reviewing files that changed from the base of the PR and between 92fb6b2f493ea22a44233ca804b24086e0899c89 and d03048e852f9086a871c946031b649790d70f08b.
📒 Files selected for processing (3)
agent/context_compressor.pyagent/conversation_compression.pytests/agent/test_compress_provider_context.py
🚧 Files skipped from review as they are similar to previous changes (2)
- agent/conversation_compression.py
- tests/agent/test_compress_provider_context.py
d03048e to
a40f7b9
Compare
a40f7b9 to
d9f7b5a
Compare
d9f7b5a to
0c93011
Compare
0c93011 to
346160f
Compare
346160f to
c2c55b3
Compare
When an ordinary agent reply already poses a question with a short list of options, the Discord adapter now appends the same clickable buttons the clarify tool would — without any clarify-tool or gateway involvement. - _detect_inline_choices: parses numbered/circled/lettered/bulleted lists and inline "A or B?" options, gated on a question mark or colon intro so incidental lists (steps, citations) don't sprout buttons - AutoChoiceView: picking a button injects the option as a fresh user turn via handle_message (no clarify entry); "Other" dismisses for free typing - _maybe_send_choice_buttons wired into send(); skips nonconversational msgs - auto_choice_buttons config flag (DISCORD_AUTO_CHOICE_BUTTONS), default on - respects existing user/role allowlists for button clicks Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
27 tests for the auto-choice path: parser detection/gating/dedup/cap, button-label fitting, AutoChoiceView construction, click→inject and Other dismissal with auth gating, _inject_user_choice event building, and _maybe_send_choice_buttons attach/skip/flag behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the loose colon-intro / inline "A or B?" heuristics with explicit
format prefixes so auto-choice buttons only appear when the agent opts in:
· "? " on a line -> single-select prompt
· "?? " on a line -> multi-select prompt
· no prefix -> no buttons (incidental lists, guesses, steps left alone)
_detect_inline_choices now returns (choices, multi_select); _detect_or_choices
is removed. The follow-up embed gains a title ("❓ Hermes asks", with
"(multi-select)" suffix) and echoes the question text with the prefix stripped,
fixing the previously contentless prompt.
AutoChoiceView gains a multi_select mode: choice clicks toggle selection
(green = picked) instead of injecting, a "✅ Confirm (N selected)" button joins
the picks with the ideographic comma (、) and injects them as one user turn.
Single-select and ✏️ Other behaviour, plus the auth gating, are unchanged.
Tests updated for the new prefix gating and tuple return, with added coverage
for multi-select toggle/deselect/confirm and the embed question echo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the session row when the model changes mid-session (/model command or provider failover), so token counts and costs are attributed to the correct model. Follows the same session-rotation pattern used by compression: end old session (end_reason=model_switch) -> create child with parent_session_id -> rotate agent.session_id. Fixes NousResearch#28637, NousResearch#48248, NousResearch#34850 Supersedes NousResearch#35256 Co-Authored-By: Claude <noreply@anthropic.com>
…/discord Bridge the whitelist notification to Agent D's send_whitelist_decision: when the notify target is telegram/discord, reach the LIVE in-process adapter via gateway.run._gateway_runner_ref() and send an interactive ✅Approve/⛔Ignore/ ➖Skip card whose taps call approve_pending/ignore_pending. Falls back to the existing plain-text _send_to_platform path on any failure (no live gateway, adapter missing the method, or card error). LINE keeps plain text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the dashboard 'Approve' button doing nothing: approve_pending
resolved scope only from entry['source_type']; a legacy unauthorized_seen row
(written before the pending feature, e.g. Cbb218) has no source_type, so
approve_pending returned {approved:False} and added nothing — while the handler
still returned an idempotent 200, so the UI looked successful but the store was
untouched.
Fix: infer the scope from the LINE id prefix (U->dm, C->group, R->room) when
source_type is missing/invalid, in both approve_pending (so approve actually
whitelists) and list_pending (so the UI shows the right type + the
already-whitelisted filter works). Truly unresolvable ids stay a clean no-op
with reason 'unresolved scope'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…filter)
Records filtered session_key.startswith('line:'), but real keys are
'agent:main:line:group:C…' (platform is a MIDDLE segment), so nothing matched
and the panel always showed 'No LINE sessions yet'. Match ':line:' segment
instead. Test updated to use realistic agent:main:line:… keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…user' The dashboard URL/UI scope vocabulary is user/group/room; the WhitelistStore's is dm/group/room. add_whitelist/remove_whitelist/list_whitelist passed the UI 'user' straight to the store, which only knows 'dm' -> WhitelistError 'unknown scope: user' -> 400, so no user could ever be added/removed from the dashboard (only groups worked, since 'group' matches both). Translate user->dm at the handler boundary. Fake store + tests updated to the store vocabulary; added a user add+remove regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Allowlist panel duplicated the group/user/room lists already shown (with names, source/lock badges, and delete) by the Authorized panel. Slim the Allowlist panel to just the 'add a new entry' form + a hint pointing to the Authorized panel for managing existing entries. Removes the double display.
The card callback compared the TAPPER's id (a Telegram/Discord user id) against
the LINE 'admins' list, so the admin who received the card got '⛔ You are not
a whitelist admin' when tapping Approve — cross-platform identity mismatch.
Add WhitelistStore.is_notify_target(platform, id) + is_card_admin(platform, id)
= LINE admin OR the unauthorized_notify recipient on that platform. Telegram
and Discord card callbacks now authorize via is_card_admin('telegram'/'discord',
caller_id), so the notify recipient can act on the card. Falls back to is_admin
on an older store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- card_admins(): per-platform admin id table (line/telegram/discord) under platforms.line.card_admins, for interactive-card callback authorization. - set_card_admins(platform, ids): persist a platform's admin list. - is_card_admin() now checks the card_admins table first (managed mechanism), then falls back to LINE admins + notify recipient — so the card callback authorizes via the table with zero adapter changes. - get_settings()/set_setting(): read + write the config-backed, hot-reload Dashboard-editable settings (requires_mention, unauthorized_notify, retention_days, observe_unmentioned, allow_all_users, media); set_setting refuses non-allowlisted keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The whitelist adapter's requires_mention gate depends on _bot_mentioned(), which matches LINE mentionees against our own bot userId (fetched via GET /v2/bot/info at connect). If that fetch fails, _bot_user_id stays None and _bot_mentioned() can never return True — so an authorized group with requires_mention=True would have EVERY message routed to observe-record and silently never answered (over-correction / whole-group silence). Fail-open: when requires_mention is on but _bot_user_id is None, skip the gate and trigger the agent (pre-whitelist behaviour) instead of silencing the group, and log one prominent warning per connection cycle. Also log whether the bot userId resolved at connect (gate active vs fail-open). Adds test_authorized_group_no_mention_failopen_when_bot_id_unknown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… guard)
_download_media() called cache_image_from_bytes() for EVERY media kind. That
function magic-byte-validates and raises on non-image data, so LINE .m4a voice
notes were rejected ("Refusing to cache non-image data as .m4a"), returned
None, and degraded to a bare "[audio]" placeholder — the audio never reached
the gateway's VOICE -> STT (Groq Whisper) pipeline.
Switch to cache_media_bytes(), which dispatches by kind: audio ->
cache_audio_from_bytes (no image validation), video -> cache_video_from_bytes,
file -> cache_document_from_bytes, image -> cache_image_from_bytes (unchanged).
MessageType mapping already maps audio -> VOICE, so a correctly cached path now
flows to STT. §7 media policy (video filter / photo+file retention) is
untouched: msg_type still drives observe-record filtering.
Adds TestDownloadMediaRouting (audio cached not rejected, image still
validates, fetch failure -> None).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion fallback) Two-part fix so attached PDFs are read reliably, platform-independently: 1. LINE adapter (#1 filename loss): the trigger path dropped the file's real fileName — every "file" cached as an anonymous .bin with media_type "file" (not application/pdf), so the agent couldn't tell it was a PDF. Now capture msg.fileName + guess MIME and pass them to cache_media_bytes(), caching as receipt.pdf / application/pdf like Telegram. _download_media returns (path, mime). 2. Gateway auto-extraction (#2/#3): GatewayRunner._auto_extract_pdf() runs at inbound time (not model-decided) — text-layer PDFs are inlined via pymupdf (free, instant); scanned PDFs with no text layer fall back to rendering each page and reading it through the vision auxiliary (_vision_read_scanned_pdf, whatever auxiliary.vision resolves to). Best-effort, never breaks the flow; pymupdf-unavailable degrades to None. Tests: TestDownloadMediaRouting (filename/mime preserved) + test_auto_pdf_ extraction (text inline / scanned->vision / non-pdf / no-pymupdf). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lsx) The gateway-side auto-extraction was PDF-only. Generalize it into _auto_extract_document(), a type dispatcher that inlines an attached document's content using whatever extractor is already bundled in the venv (no lazy install): * PDF -> pymupdf text / render + vision for scanned pages * text family -> read directly (txt/md/csv/json/xml/yaml/log/code…) * DOCX -> python-docx (paragraphs + tables) * XLSX -> openpyxl (all sheets, tab-separated) Formats with no bundled extractor (PPTX, legacy .doc/.ppt/.xls, archives, video) return None and fall back to the path-pointing context note — the agent can still open them with terminal tools. Pairs with the generic LINE filename/MIME fix so every attachment now reaches the agent typed correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egacy _auto_extract_document() now covers EVERY Office format. New PowerPoint and all legacy binary Office (.doc/.ppt/.xls, plus .odt/.ods/.odp/.rtf) route to _office_via_libreoffice(): headless `soffice --convert-to pdf` -> the existing PDF text/vision pipeline (so image-only slides still get read via vision). One deployer-installed dependency (LibreOffice) covers new + old uniformly; docx/xlsx/pdf/text stay on the faster native path. soffice is a deliberate one-time deploy install — distinct from the disabled agent-runtime lazy installs (security.allow_lazy_installs=false). Missing soffice / conversion failure degrade gracefully to the context note. Isolated HOME per convert + 90s timeout + temp cleanup guard concurrency and leaks. Tests: pptx/legacy-doc route to the bridge; missing-soffice -> None; plus the existing docx/xlsx/pdf/text/csv regression set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t PDF Rendering .xls/.ods to PDF clips each cell to its column width (e.g. "Aryaduta Bali" -> "Aryaduta B"), silently losing overflowing cell text. Convert spreadsheets to XLSX instead and read full cell values with openpyxl (re-entering the native dispatcher branch). Documents/presentations keep the PDF path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two group UX fixes for requires_mention chats: 1. Quote-reply of the bot's OWN message counts as an implicit @mention, so users don't have to re-type @toothless to follow up on something it said. _LineClient now records the ids it sent (from the reply/push response `sentMessages`); a message whose `quotedMessageId` is one of ours passes the mention gate. Quoting another member's message does NOT (stays observe-only). 2. Unmentioned image/file uploads (LINE can't @ an attachment) are now DOWNLOADED and PRE-EXTRACTED at observe time — image -> vision description, PDF -> text — and that content is inlined into the observed row. A later "@toothless what was that receipt?" turn can then reference the just-sent file (previously observe stored only a "[image]"/"[file]" placeholder, so the file was invisible to the follow-up). Still observe-only (no trigger, mention gate unchanged); rate-limited per chat (LINE_OBSERVE_MEDIA_MAX / _WINDOW, default 12/hour) so a media flood can't run unbounded extraction; video/audio still dropped per §7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the "pre-extract every observed media" approach with an on-demand, time-windowed backfill — cheaper and abuse-resistant: - Observe path is lightweight again: unmentioned image/file uploads are recorded as a "[image]"/"[file: name]" placeholder + their LINE platform_message_id ONLY (no download/vision at observe time). - When the bot IS triggered (@mention / quote-reply) and the triggering group message carries no media of its own, _backfill_recent_media() looks back at the recently-observed image/file uploads within a window, re-downloads them, and appends them to the turn's media_urls — so the gateway's normal pipeline (vision for images, doc-extract for PDFs) reads them. This makes "@toothless what's the total on that receipt?" work after a silent image upload. - Window is Dashboard-editable: media_backfill_window_minutes (default 1, 0 disables), config hot-reload, added to the store settings + Settings panel schema + coercion. Complements quote-reply=implicit-mention for the "剛剛那張" case where the user didn't quote. Mention gate / whitelist / retention unchanged; capped at 3 recent items per turn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the deterministic backfill do the extraction itself (on-demand, at the
trigger turn) and inject the result as context, rather than re-attaching raw
media every turn — the gateway's vision pass has NO cache and would re-bill it.
- Images: vision-read inside the backfill and the analysis is injected via
MessageEvent.channel_context (gateway prepends it to the turn). Doubles as
the auxiliary "recent uploads" prompt hint. The raw image is NOT re-attached,
so vision runs at most once per image.
- Files/PDFs: downloaded and returned as media_urls so the agent extracts them
with its file tools; named in the hint line too.
- Cost control ("抽過的快取不重抽"): both the LINE download and the vision
extraction are memoized by the immutable LINE message id (bounded FIFO,
LINE_MEDIA_BACKFILL_CACHE_MAX, default 256), so the same recent upload pulled
into several trigger turns inside the window never re-downloads or re-visions.
Program-driven, not model-driven: the adapter decides deterministically; the
weak model never has to choose to search. Still gated by mention/whitelist/
scope/retention and the Dashboard window. +2 tests (extract-once cache, file
attach); 192 LINE tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes for the "bot said 已記好 but File-mutation verifier warned NOT
modified" report, where LINE travel-accounting turns showed a scary red footer
even though the data landed in the Obsidian vault via turbovault MCP.
B — turbovault edit_note reliability. A weak model repeatedly called
mcp__turbovault__edit_note with malformed `edits` (SEARCH:/REPLACE: labels, or
[{"old_string","new_string"}] JSON borrowed from the local file tool), which
turbovault rejects ("No SEARCH/REPLACE blocks found in input"), forcing a
full-note write_note overwrite every edit. Add a deterministic arg normalizer
(tools/turbovault_edit_normalize.py) that rewrites the known malformed shapes
into canonical aider SEARCH/REPLACE blocks, applied in the MCP handler
(tools/mcp_tool.py) before dispatch. Unrecognized input passes through
untouched — turbovault stays the final authority and the write_note fallback is
preserved. Verified live: normalizer output applies on the real turbovault
(blocks_applied: 1).
C — file-mutation verifier false positive. The verifier only tracks local
write_file/patch; it never saw turbovault write_note/edit_note successes, so a
co-occurring spurious local patch failure (raw vault path that doesn't exist on
the host) triggered the footer even though the vault write landed. Track a
per-turn _turn_vault_mutation_succeeded flag (set in _record_file_mutation_result
for a successful mcp__turbovault__write_note/edit_note) and gate the footer on
it in turn_finalizer. The safety net is intact: when nothing succeeded anywhere,
the footer still fires.
Local-file vault fallback (offline resilience) is deliberately left untouched.
+56 tests (normalizer shapes + passthrough; verifier suppression + safety net).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LINE's text bubble renders zero Markdown, so a GFM pipe table from the model landed in the chat as literal "| Item | Cost |" / "|------|" rows. Discord and Telegram already solve this with the shared convert_table_to_bullets() helper (gateway/platforms/helpers.py, added in PR NousResearch#53284) — LINE was simply never wired into it. Wire LINE into the same shared converter rather than growing a LINE-specific table implementation. The call goes at the top of strip_markdown_preserving_urls(), which every outbound send path already funnels through (postback reply, _send_text_chunks, format_message, standalone send), so all of them are covered by one hook. Order matters: the converter runs BEFORE the existing code-fence un-fencing. The shared converter deliberately skips fenced blocks, but once the fences are stripped a table inside a code block would look like a real table and be wrongly converted. Running it first preserves fence-skipping. The two steps compose cleanly: the converter emits "**heading**" + "• field: value"; the existing strip removes the bold markers, and the "•" bullets pass through _MD_BULLET_RE untouched (it only matches -/*/+ markers). +3 tests (table → bullets with no raw pipes; fenced table left verbatim; no-table behaviour unchanged). 213 LINE + table-helper tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…am rebase Upstream 9ce0e67 added get_conversation_root() which walks the full parent_session_id lineage (compression rotations + delegate children). Our model-switch context-continuity fix had narrowed _session_lineage_root_to_tip() to model_switch-only links, which broke the new upstream tests. Add only_model_switch flag: replay path keeps the model_switch-only filter, get_conversation_root walks every link.
…pstream resume refactor
…the summary The compaction path called `memory_manager.on_pre_compress(messages)` but discarded its return value (conversation_compression.py), so the hook ran and every provider's contributed text went nowhere. This made mem4's ⑤ headline benefit — feeding its routing legend / cold-tier summaries into the compaction summary so the map survives compression — inert. (Confirms the Fable 5 spike review; corresponds to upstream issue NousResearch#23367.) Fix (additive, backward-compatible): - conversation_compression.py: capture the on_pre_compress() return string (guarded; a provider failure never blocks compaction) and pass it to compress(provider_context=...). The existing TypeError fallback for strict plugin context engines is left untouched (graceful drop for those). - context_compressor.py: compress() gains an optional `provider_context` param, stashed on `self._pending_provider_context` (reset per call). _generate_summary appends it to the summarizer prompt with an instruction to reproduce it verbatim in a "## Memory Provider Context" section — it is provider context to carry forward, not conversation to digest. Tests: 5 new (injection present/absent/empty, compress threads it through). No regression: test_context_compressor.py (120), test_compress_focus.py (5), summary-continuity + temporal-anchoring (7) all green. Part of the mem4 ⑤ work (fork feature branch). HOLD upstream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c2c55b3 to
6cf37ef
Compare
e1f3313 to
d2712ab
Compare
|
Superseded by upstream: hermes-agent now injects the memory provider's |
What
Capture the memory-provider
on_pre_compress()hook's return value and inject it into the compaction summary. Previously the compaction path calledon_pre_compress()but discarded its return string, so the hook ran but its text went nowhere — the routing legend / cold-tier pointers a provider wants to survive compression were silently dropped.Why
agent/memory_provider.pydocumentson_pre_compress(messages) -> stras returning free text to fold into the compression summary. The compaction path invoked it for side effects only and threw away the result, so any provider relying on it (for example, a routed-memory provider that wants its legend to survive context compression) was a no-op across compaction. Aligns with upstream NousResearch#23367 (extract-before-compress). Additive and backward-compatible: providers that return an empty string are unaffected.Changes
agent/conversation_compression.py— capture theon_pre_compress()return value and pass it through to the summary assembler.agent/context_compressor.py— thread the provider text into the compaction summary.tests/agent/test_compress_provider_context.py— new coverage (5 tests): hook text injected, empty return is a no-op, provider exceptions are swallowed, ordering.Testing
pytest tests/agent/test_compress_provider_context.py -q-> 5 passed.Note
Core-only, decoupled from any memory plugin. This is the compression fix extracted from the superseded PR #2 (the mem4 four-tier provider), which now lives as a standalone plugin at github.com/sam7894604/hermes-plugin-mem4. This PR carries only the core compression fix so it can stand on its own.
Summary by CodeRabbit
New Features
Bug Fixes
Tests