Discord human in loop#125
Conversation
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.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
ArnabChatterjee20k has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
@greptileai review |
Greptile SummaryThis 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.
Confidence Score: 3/5The 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
Reviews (1): Last reviewed commit: "updated" | Re-trigger Greptile |
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.
record_answer_message_ids/lookup_answer_issueto theAttemptTrackertrait backed by a newanswer_message_idsTEXT column (V8 migration) with delimiter-padded packing for whole-ID LIKE matching.notify_answeracross theNotifiertrait,CompositeNotifier,InstrumentedNotifier, and the Discord implementation to returnVec<String>of sent message IDs; removes embed-based answer formatting in favour of plain-text chunks.assemble_reply_chain/with_reply_chaintoIssueProcessor, applied to the fix, reply, and Q&A answer paths; stores the full (untruncated) answer inerror_messagefor 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_issueuses.ok()to convert thequery_rowresult, which collapses real SQLite failures (I/O errors, schema mismatches) intoOk(None)indistinguishably from a genuine cache miss. When this happens,assemble_reply_chainsilently 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— thelookup_answer_issueerror handling needs a targeted fix before ship.Important Files Changed
record_answer_message_idsandlookup_answer_issue; the lookup silently swallows real DB errors via.ok()assemble_reply_chain/with_reply_chainfor Discord reply-chain context; addsattempt_idto RAG pipeline; stores full answer text instead of 500-char summarynotify_answernow returns sent message IDsNotifiertrait andCompositeNotifierto returnVec<String>message IDs fromnotify_answerreply_to_message_idandreply_to_channel_idfrom Discord message references into issue metadataanswer_message_ids TEXTcolumn tofix_attemptsviaALTER TABLESequence 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)%%{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)Comments Outside Diff (2)
crates/claudear-engine/src/processing.rs, line 103-113 (link)strip_discord_mentionsleaves double spaces in outputWhen 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
crates/claudear-storage/src/sqlite.rs, line 1023-1027 (link)lookup_answer_issueswallows real SQLite errors.ok()converts everyrusqlite::Error— including I/O failures, lock errors, and schema mismatches — intoNone, making them indistinguishable from a genuine cache miss. When this happens,assemble_reply_chainsilently 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 onlyQueryReturnedNoRowstoOk(None)and let other errors propagate, matching the pattern used elsewhere in this file.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "updated" | Re-trigger Greptile