Skip to content

Discord human in loop#125

Open
ArnabChatterjee20k wants to merge 11 commits into
mainfrom
discord-human-in-loop
Open

Discord human in loop#125
ArnabChatterjee20k wants to merge 11 commits into
mainfrom
discord-human-in-loop

Conversation

@ArnabChatterjee20k

@ArnabChatterjee20k ArnabChatterjee20k commented Jul 14, 2026

Copy link
Copy Markdown
Member

Greptile Summary

This PR implements Discord "human in the loop" reply-chain context: when a user replies to one of Claudear's answer messages, the engine now walks the reply thread, pulls Claudear's prior answer and the original question from the DB (or fetches other users' messages from Discord), and prepends a labelled transcript to the agent's grounding context. It also switches Discord answer delivery from rich embeds to plain-text chunks and records the sent message IDs so any chunk reply can be mapped back to its issue.

  • Adds record_answer_message_ids / lookup_answer_issue to the AttemptTracker trait backed by a new answer_message_ids TEXT column (V8 migration) with delimiter-padded packing for whole-ID LIKE matching.
  • Changes notify_answer across the Notifier trait, CompositeNotifier, InstrumentedNotifier, and the Discord implementation to return Vec<String> of sent message IDs; removes embed-based answer formatting in favour of plain-text chunks.
  • Adds assemble_reply_chain / with_reply_chain to IssueProcessor, applied to the fix, reply, and Q&A answer paths; stores the full (untruncated) answer in error_message for later grounding.

Confidence Score: 4/5

Safe to merge with the lookup error-handling fixed; the rest of the change is well-structured and covered by tests.

lookup_answer_issue uses .ok() to convert the query_row result, which collapses real SQLite failures (I/O errors, schema mismatches) into Ok(None) indistinguishably from a genuine cache miss. When this happens, assemble_reply_chain silently falls through to the Discord API fetch with no warning emitted, so storage regressions during the reply-chain walk are invisible in logs and metrics.

crates/claudear-storage/src/sqlite.rs — the lookup_answer_issue error handling needs a targeted fix before ship.

Important Files Changed

Filename Overview
crates/claudear-storage/src/sqlite.rs Adds record_answer_message_ids and lookup_answer_issue; the lookup silently swallows real DB errors via .ok()
crates/claudear-engine/src/processing.rs Adds assemble_reply_chain / with_reply_chain for Discord reply-chain context; adds attempt_id to RAG pipeline; stores full answer text instead of 500-char summary
crates/claudear-integrations/src/notifier/discord.rs Replaces embed-based answer delivery with plain-text chunked messages; notify_answer now returns sent message IDs
crates/claudear-integrations/src/notifier/mod.rs Updates Notifier trait and CompositeNotifier to return Vec<String> message IDs from notify_answer
crates/claudear-integrations/src/source/discord.rs Captures reply_to_message_id and reply_to_channel_id from Discord message references into issue metadata
migrations/V8__answer_message_ids.sql Adds answer_message_ids TEXT column to fix_attempts via ALTER TABLE

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant U as User (Discord reply)
    participant DS as DiscordSource
    participant IP as IssueProcessor
    participant ST as SqliteTracker
    participant DC as DiscordClient
    participant DN as DiscordNotifier

    U->>DS: Reply message (references parent_message_id)
    DS->>DS: message_to_issue() sets reply_to_message_id metadata
    DS->>IP: Issue (with reply metadata)

    IP->>IP: assemble_reply_chain(issue)
    IP->>ST: lookup_answer_issue(parent_message_id)
    alt Claudear's own answer (in DB)
        ST-->>IP: Ok(Some(src, issue_id))
        IP->>ST: get_attempt(src, issue_id)
        IP->>ST: get_embedding(src, issue_id)
        IP-->>IP: Build [User]: …\n[Claudear]: …
    else Other user's message (not in DB)
        ST-->>IP: Ok(None)
        IP->>DC: get_message(channel, parent_id)
        DC-->>IP: DiscordMessage (content + message_reference)
        IP-->>IP: Walk to next ancestor
    end

    IP->>IP: with_reply_chain(issue, context) prepends transcript
    IP->>DN: notify_answer(issue, reply)
    DN-->>IP: "Ok(Vec<message_ids>)"
    IP->>ST: record_answer_message_ids(source, issue_id, ids)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant U as User (Discord reply)
    participant DS as DiscordSource
    participant IP as IssueProcessor
    participant ST as SqliteTracker
    participant DC as DiscordClient
    participant DN as DiscordNotifier

    U->>DS: Reply message (references parent_message_id)
    DS->>DS: message_to_issue() sets reply_to_message_id metadata
    DS->>IP: Issue (with reply metadata)

    IP->>IP: assemble_reply_chain(issue)
    IP->>ST: lookup_answer_issue(parent_message_id)
    alt Claudear's own answer (in DB)
        ST-->>IP: Ok(Some(src, issue_id))
        IP->>ST: get_attempt(src, issue_id)
        IP->>ST: get_embedding(src, issue_id)
        IP-->>IP: Build [User]: …\n[Claudear]: …
    else Other user's message (not in DB)
        ST-->>IP: Ok(None)
        IP->>DC: get_message(channel, parent_id)
        DC-->>IP: DiscordMessage (content + message_reference)
        IP-->>IP: Walk to next ancestor
    end

    IP->>IP: with_reply_chain(issue, context) prepends transcript
    IP->>DN: notify_answer(issue, reply)
    DN-->>IP: "Ok(Vec<message_ids>)"
    IP->>ST: record_answer_message_ids(source, issue_id, ids)
Loading

Comments Outside Diff (2)

  1. crates/claudear-engine/src/processing.rs, line 103-113 (link)

    P2 strip_discord_mentions leaves double spaces in output

    When a mention sits between two words (e.g., "hey <@!456> there"), removing the mention leaves the surrounding spaces intact, producing double spaces ("hey there"). The existing test even asserts "hey there !" as expected output. This double-space noise will be forwarded verbatim to the LLM as grounding context in the reply chain. Adding a post-pass that collapses multiple spaces (or replacing the mention with a single space) would clean this up.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/claudear-engine/src/processing.rs
    Line: 103-113
    
    Comment:
    **`strip_discord_mentions` leaves double spaces in output**
    
    When a mention sits between two words (e.g., `"hey <@!456> there"`), removing the mention leaves the surrounding spaces intact, producing double spaces (`"hey  there"`). The existing test even asserts `"hey  there !"` as expected output. This double-space noise will be forwarded verbatim to the LLM as grounding context in the reply chain. Adding a post-pass that collapses multiple spaces (or replacing the mention with a single space) would clean this up.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  2. crates/claudear-storage/src/sqlite.rs, line 1023-1027 (link)

    P1 lookup_answer_issue swallows real SQLite errors

    .ok() converts every rusqlite::Error — including I/O failures, lock errors, and schema mismatches — into None, making them indistinguishable from a genuine cache miss. When this happens, assemble_reply_chain silently falls through to the Discord API fetch path instead of propagating the error or at least emitting a warning. The correct approach is to map only QueryReturnedNoRows to Ok(None) and let other errors propagate, matching the pattern used elsewhere in this file.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/claudear-storage/src/sqlite.rs
    Line: 1023-1027
    
    Comment:
    **`lookup_answer_issue` swallows real SQLite errors**
    
    `.ok()` converts every `rusqlite::Error` — including I/O failures, lock errors, and schema mismatches — into `None`, making them indistinguishable from a genuine cache miss. When this happens, `assemble_reply_chain` silently falls through to the Discord API fetch path instead of propagating the error or at least emitting a warning. The correct approach is to map only `QueryReturnedNoRows` to `Ok(None)` and let other errors propagate, matching the pattern used elsewhere in this file.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
crates/claudear-storage/src/sqlite.rs:1023-1027
**`lookup_answer_issue` swallows real SQLite errors**

`.ok()` converts every `rusqlite::Error` — including I/O failures, lock errors, and schema mismatches — into `None`, making them indistinguishable from a genuine cache miss. When this happens, `assemble_reply_chain` silently falls through to the Discord API fetch path instead of propagating the error or at least emitting a warning. The correct approach is to map only `QueryReturnedNoRows` to `Ok(None)` and let other errors propagate, matching the pattern used elsewhere in this file.

Reviews (3): Last reviewed commit: "updated" | Re-trigger Greptile

Add a V8 migration with an `answer_message_ids` column on `fix_attempts`
and `record_answer_message_ids` / `lookup_answer_issue` tracker methods, so
a user's reply to any of Claudear's answer chunks can be mapped back to the
issue it belongs to without fetching from Discord.

Stored as a comma-delimited list with delimiter guards (",id1,id2,") so
reverse lookups match whole ids only. Includes a round-trip test covering
multi-chunk lookup and partial-id rejection.
reply_chain_enabled (default true) and reply_chain_max_depth (default 15) on
the Discord source config, surfaced through discord_merged() and documented
under [issues.discord] in the example config.
When an ingested Discord message is a reply, record reply_to_message_id and
reply_to_channel_id in issue metadata (no API call) so the engine can
reconstruct the reply thread later. The reply channel falls back to the
message's own channel when the reference omits it.
Assemble the reply thread into a labelled transcript and inject it into the
QA, fix, and action-reply contexts. Claudear's own answers resolve purely
from the DB (original question + full answer); other users' messages are
fetched from Discord, walking parents until a Claudear answer is reached,
depth-capped with a cycle guard and graceful handling of deleted parents.

Also fixes a latent bypass: InstrumentedNotifier did not forward
notify_answer, so answers were delivered via the plain-text trait default
and DiscordNotifier::notify_answer never ran. notify_answer now returns the
sent message ids (recorded for the reply-chain mapping) and is forwarded
through the wrapper; Discord sends the plain "Answer for <id>:" text as a
native reply. The full answer is stored (dropping the pre-storage 500-char
truncation) so the prior turn is complete in reply-chain context.
Comment thread crates/claudear-storage/src/sqlite.rs
Comment thread crates/claudear-storage/src/sqlite.rs
Replace the default_reply_chain_* helper fns with DEFAULT_REPLY_CHAIN_ENABLED
/ DEFAULT_REPLY_CHAIN_MAX_DEPTH constants. serde's per-field `default = "..."`
requires a fn path, so drop it and give DiscordSourceConfig a manual Default
impl (used by the struct-level `#[serde(default)]`), which also makes the Rust
and serde defaults consistent.
tracing::warn!(short_id = %issue.short_id, error = %e, "Failed to deliver answer");
}
}
let summary: String = answer.chars().take(500).collect();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storing full answer for two purposes -> full context for agents in multiple runs + discord doesn't allow after a threshold. so we can check the data internally as well

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@abnegate

Copy link
Copy Markdown
Member

@greptileai review

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements Discord "human in the loop" reply-chain context: when a user replies to any of Claudear's answer chunks, the engine now resolves the full prior conversation (Claudear's own answers from the DB, other users' messages via Discord API) and prepends it as grounding context before invoking the agent.

  • Storage layer: migration V8 adds answer_message_ids TEXT to fix_attempts; record_answer_message_ids stores sent message IDs (idempotent, comma-delimited with delimiter guards for safe LIKE lookups); lookup_answer_issue reverse-maps any chunk ID back to the issue. mark_answered now stores the full answer text rather than a 500-char truncation.
  • Notifier contract: notify_answer return type changed from Result<()> to Result<Vec<String>>; Discord notifier returns the IDs of every chunk it sent; CompositeNotifier and InstrumentedNotifier updated to collect and forward those IDs; all other notifiers default to returning an empty vec.
  • Engine: assemble_reply_chain walks the reply thread newest-first, resolving Claudear turns from the DB and other users' turns from the Discord API, stopping once a Claudear answer is found; strip_discord_mentions cleans mention tokens; reply-chain grounding is wired into the Q&A, handle-reply, and fix-action pipelines.

Confidence Score: 3/5

The reply-chain feature is well-structured and thoroughly tested, but the storage write path has a silent correctness gap that would cause the feature to silently stop working if the fix_attempts row is ever absent at call time.

The record_answer_message_ids function performs a SELECT then a plain UPDATE with no check on rows affected. rusqlite::execute returns Ok(0) when nothing matches, so the caller's if let Err(e) warning path never fires and the merged ID list is discarded without any log line. The architecture contract normally guarantees the row exists, but this silent-discard path means any violation causes the reply-chain feature to stop working with no observable signal.

crates/claudear-storage/src/sqlite.rs — specifically the record_answer_message_ids UPDATE and its lack of a rows-affected check; crates/claudear-engine/src/processing.rs — strip_discord_mentions greedy > match.

Important Files Changed

Filename Overview
crates/claudear-engine/src/processing.rs Core reply-chain logic: adds assemble_reply_chain (DB-first, Discord-fallback), with_reply_chain, strip_discord_mentions, wires reply chain into Q&A and fix/action pipelines, stores full (non-truncated) answers, and records sent message IDs. strip_discord_mentions greedy > match is a minor edge-case concern.
crates/claudear-storage/src/sqlite.rs Adds record_answer_message_ids (idempotent read-then-update, comma-delimited with delimiter guards) and lookup_answer_issue (LIKE with ESCAPE). The plain UPDATE in record_answer_message_ids silently no-ops when no fix_attempts row exists, discarding IDs with no log warning.
crates/claudear-integrations/src/notifier/mod.rs notify_answer signature changed from Result<()> to Result<Vec>; CompositeNotifier re-implemented to collect IDs from all notifiers concurrently; default impl returns empty Vec. Clean change.
crates/claudear-integrations/src/notifier/discord.rs notify_answer updated to collect and return message IDs for each sent chunk. Straightforward change.
crates/claudear-integrations/src/telemetry.rs Adds notify_answer override on InstrumentedNotifier to forward to the inner notifier and return IDs, preventing the trait default from swallowing them.
crates/claudear-integrations/src/source/discord.rs message_to_issue now records reply_to_message_id and reply_to_channel_id metadata from Discord message_reference, with fallback to own channel. Well-tested.
crates/claudear-storage/src/lib.rs Adds record_answer_message_ids and lookup_answer_issue to AttemptTracker trait with default no-op implementations.
crates/claudear-storage/src/migrator.rs Adds V8 migration entry and updates tests to assert version 8 and the new answer_message_ids column.
migrations/V8__answer_message_ids.sql Adds nullable TEXT column answer_message_ids to fix_attempts via ALTER TABLE. Simple additive migration.
crates/claudear-config/src/config.rs Adds reply_chain_enabled and reply_chain_max_depth to DiscordSourceConfig and DiscordConfig with sensible defaults (true, 15).
claudear.example.toml Documents the two new reply_chain_* config options under the Discord source section.

Reviews (1): Last reviewed commit: "updated" | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants