Skip to content

feat(reports): support threads digest and reviewed answer drafts - #162

Open
HarshMN2345 wants to merge 15 commits into
appwrite:mainfrom
HarshMN2345:feat/support-digest
Open

HarshMN2345 wants to merge 15 commits into
appwrite:mainfrom
HarshMN2345:feat/support-digest

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Surfaces Discord support threads that need a reply and drafts answers for a person to review. Configured under [reports.support_digest], off by default. Claudear never posts to the forum.

Digest

  • Reads open threads, messages and team authors from the Appwrite project the threads bot syncs the forum into (endpoint, project_id, database_id, api_key). Claudear needs no access to the forum.
  • Every interval_hours (default 1), ranks threads active in the last days by: no reply / no team reply, wait time, bumps, impact keywords (production, outage, data loss, billing, lockout, security), and other users reporting the same issue. Team is team_user_ids plus thread authors holding a team_roles role.
  • Posts the top max_entries to Claudear's Discord notifier channel only when a new thread enters the list, plus a short "looks resolved" list and how many drafts wait for review.

Drafts (drafts = true, needs api_key with rows read/write)

  • Each scan, the QA agent answers up to max_drafts listed threads whose draft is missing or older than their latest message, grounded in code search and indexed Discord discussions. Answers go to the threads project's drafts table as pending.
  • New dashboard Support page: pending and failed drafts with their conversation. Admins edit the answer and approve or reject it.
  • Approving only updates the row. The threads project's function (appwrite-labs/threads#13) posts approved answers to the thread and marks them sent or failed.

Checked

  • Against the live threads project: 46 open threads and 367 messages read in about 1.5s, 27 ranked as needing a reply.
  • Against a local Appwrite project with the same tables: saving, reading, counting and listing drafts, approving with an edited answer, reviewing a missing draft, and re-drafting all behave as expected.
  • The Support page rendered against a mock API: approving sent the edited answer with the CSRF header and the draft left the list.
  • Clippy (-D warnings) and fmt pass for claudear-config, claudear-integrations and claudear-engine; dashboard typecheck and its 258 tests pass. Draft generation with the real agent was not run.

Scans a Discord support forum, ranks open threads by how urgently they
need a reply (no reply yet, wait time, bumps, impact keywords, others
reporting the same problem) and posts the top ones to Discord when a
thread enters the list. Also lists threads that look resolved so they
can be closed. Report-only: threads never enter the fix pipeline.

Configured under [reports.support_digest], off by default.
@hansi-codes

hansi-codes Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

🔵 Tier A · Mergeable after minor fixes

The previously reported minor test-coverage finding remains open for the new client paths.

Adds an opt-in Appwrite-backed support-thread digest that ranks threads needing replies, with optional QA-generated answer drafts for human review. A new Support dashboard lets admins inspect conversations, edit drafts, and approve or reject them; posting approved answers remains delegated to the threads project's function. The pull request also expands configuration, API, scheduling, shutdown, telemetry, documentation, and live-QA controls.

Latest changes: The newest commits add unit tests for title resolution, triage statuses, unanswered streaks and bumps, and impact ranking.

Verdict New comments Fixed Still open
✅ Approved 0 0 1
Fix with agent prompt
### Issue 1
crates/claudear-integrations/src/reports/support.rs:187-318
**No tests for triage/scoring or the new Discord client paths**

The triage logic has many branches: team promise vs. answer, LikelyResolved vs. STILL_BROKEN, bump counting, the streak index, and impact regexes. `is_solved` and the archived `since` paging and `get_member_roles` are new as well. None of it has tests, although comparable modules in this repo do. Table-style unit tests for `triage` and `is_solved`, plus a mock-HTTP test for `list_public_archived_threads_since`, would guard the ranking.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
📂 Walkthrough · 17
File Change
README.md Documents the support digest, reviewed drafts, live-QA guardrails, and shutdown behavior.
claudear.example.toml Adds support-digest and live-QA configuration examples.
crates/claudear-config/src/config.rs Adds support report settings, MCP search configuration, and deploy-QA timeout helpers.
crates/claudear-engine/src/api/routes.rs Exposes draft review, token-management, and declined-attempt API routes.
crates/claudear-engine/src/housekeeping.rs Schedules independent, non-overlapping support scans.
crates/claudear-engine/src/lib.rs Exports support digest, heartbeat, and shutdown modules.
crates/claudear-engine/src/support_digest.rs Reads Appwrite support conversations, tracks digest state, and creates or updates drafts.
crates/claudear-engine/src/watcher.rs Delivers digests and generates bounded draft batches with revision and shutdown handling.
crates/claudear-integrations/src/notifier/discord.rs Renders support digest notifications and pending draft counts.
crates/claudear-integrations/src/notifier/mod.rs Adds digest delivery hooks and aggregates posting outcomes.
crates/claudear-integrations/src/reports/mod.rs Exports support report helpers and types.
crates/claudear-integrations/src/reports/support.rs Implements thread ranking and resolution heuristics, with new unit tests for triage and scoring.
crates/claudear-integrations/src/telemetry.rs Instruments support digest delivery outcomes.
dashboard/src/App.tsx Registers the Support page route.
dashboard/src/components/layout/sidebar.tsx Adds Support navigation.
dashboard/src/lib/api.ts Adds draft-review and token-management API helpers.
dashboard/src/pages/support.tsx Displays editable drafts and conversations, and handles review revision conflicts.
⏳ Still open from earlier reviews · 1

Reviewed the commits since fead523 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes 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.

🔵 Tier A · Looks good to merge. Summary

Comment thread crates/claudear-engine/src/watcher.rs Outdated
Comment thread crates/claudear-integrations/src/notifier/discord.rs
Comment on lines +187 to +318
pub fn triage(&self, team: &HashSet<String>, now: DateTime<Utc>) -> Option<SupportEntry> {
let last = self.messages.last()?;
let speakers: Vec<Speaker> = self
.messages
.iter()
.map(|message| self.speaker(message, team))
.collect();
let last_speaker = speakers[speakers.len() - 1];
let replies = speakers.iter().filter(|s| **s != Speaker::Op).count();
let team_replied = speakers.contains(&Speaker::Team);

// The poster's current unanswered streak: every trailing message of theirs.
let streak = speakers
.iter()
.rposition(|s| *s != Speaker::Op)
.map_or(0, |i| i + 1);
let waiting_from = if last_speaker == Speaker::Op {
&self.messages[streak]
} else {
last
};
let waiting_hours = (now - waiting_from.timestamp).num_hours().max(0);

let mut reasons = Vec::new();
let mut score = 0.0;
let status = if last_speaker == Speaker::Team
&& !(PROMISE.is_match(&last.content) && waiting_hours >= 24)
{
reasons.push(format!("team replied last, {}d ago", waiting_hours / 24));
SupportStatus::WaitingOnUser
} else if last_speaker == Speaker::Op
&& replies > 0
&& THANKS.is_match(&last.content)
&& !STILL_BROKEN.is_match(&last.content)
{
reasons.push("poster's last message reads like it is fixed".to_string());
SupportStatus::LikelyResolved
} else {
if last_speaker == Speaker::Team {
score += 20.0;
reasons.push(format!(
"team said \"{}\" with no follow-up",
excerpt(&last.content, 60)
));
} else {
score += 30.0;
if replies == 0 {
score += 20.0;
reasons.push("no replies yet".to_string());
} else if !team_replied {
score += 10.0;
reasons.push("no team reply".to_string());
}
if last_speaker == Speaker::Community {
reasons.push("community member replied last".to_string());
}
}
SupportStatus::NeedsReply
};

if status == SupportStatus::NeedsReply {
score += (waiting_hours as f64 / 24.0).min(7.0) * 3.0;
if waiting_hours >= 24 {
reasons.push(format!("waiting {}d", waiting_hours / 24));
}

let bumps = self.messages[streak..]
.windows(2)
.filter(|pair| pair[1].timestamp - pair[0].timestamp > Duration::hours(2))
.count();
if bumps > 0 {
score += bumps.min(3) as f64 * 6.0;
reasons.push(format!("poster bumped {bumps}x"));
}

let poster_text = std::iter::once(self.title.as_str())
.chain(
self.messages
.iter()
.zip(&speakers)
.filter(|(_, s)| **s == Speaker::Op)
.map(|(m, _)| m.content.as_str()),
)
.collect::<Vec<_>>()
.join("\n")
.to_lowercase();
let impacts: Vec<&str> = IMPACT
.iter()
.filter(|(_, pattern)| pattern.is_match(&poster_text))
.map(|(label, _)| *label)
.take(3)
.collect();
if !impacts.is_empty() {
score += impacts.len() as f64 * 12.0;
reasons.push(format!("mentions {}", impacts.join(", ")));
}

let affected: HashSet<&str> = self
.messages
.iter()
.zip(&speakers)
.filter(|(m, s)| **s == Speaker::Community && ALSO_AFFECTED.is_match(&m.content))
.map(|(m, _)| m.author_id.as_str())
.collect();
if !affected.is_empty() {
score += affected.len().min(2) as f64 * 10.0;
reasons.push(format!("{} other user(s) report the same", affected.len()));
}

if self
.tags
.iter()
.any(|tag| tag.eq_ignore_ascii_case("cloud"))
{
score += 5.0;
reasons.push("Cloud".to_string());
}
}

Some(SupportEntry {
thread_id: self.id.clone(),
title: self.title.clone(),
url: self.url.clone(),
status,
score: score.round() as u32,
reasons,
last_author: last.author.clone(),
last_message: excerpt(&last.content, 200),
waiting_hours,
is_new: false,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No tests for triage/scoring or the new Discord client paths

The triage logic has many branches: team promise vs. answer, LikelyResolved vs. STILL_BROKEN, bump counting, the streak index, and impact regexes. is_solved and the archived since paging and get_member_roles are new as well. None of it has tests, although comparable modules in this repo do. Table-style unit tests for triage and is_solved, plus a mock-HTTP test for list_public_archived_threads_since, would guard the ranking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/claudear-integrations/src/reports/support.rs
Line: 187-318

Comment:
**No tests for triage/scoring or the new Discord client paths**

The triage logic has many branches: team promise vs. answer, LikelyResolved vs. STILL_BROKEN, bump counting, the streak index, and impact regexes. `is_solved` and the archived `since` paging and `get_member_roles` are new as well. None of it has tests, although comparable modules in this repo do. Table-style unit tests for `triage` and `is_solved`, plus a mock-HTTP test for `list_public_archived_threads_since`, would guard the ranking.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

🟡 Minor · testing · Reply if this doesn't apply.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 0/5

[Medium risk] Adds hourly Discord support forum digest feature.

The PR does not appear safe to merge because open [SOLVE] requests can be omitted and several previously reported delivery, collection, and lifecycle defects remain.

Fix All in Claude CodeFindings

  1. P1 Open requests can disappear ▶
  2. P1 Undelivered digest marked sent ▶
  3. P1 Digest state files collide ▶
  4. P1 Page errors drop threads ▶
  5. P1 Large activity windows panic ▶
  6. P1 Shutdown abandons digest scans ▶
  7. P1 Follow-ups override confirmed fixes ▶
  8. P2 Digest behavior lacks tests ▶
  9. P2 Skipped delivery logged as success ▶
Fix with agent prompt
### Issue 1
crates/claudear-integrations/src/reports/support.rs:169
If someone titles an unresolved request `[SOLVE] Please help with an outage`, the new `solved?` match treats it as solved. Collection then skips the thread before reading or ranking it, so an urgent request can disappear from the digest. The solved-title check should use prefixes that clearly indicate resolution.

### Issue 2
crates/claudear-integrations/src/notifier/discord.rs:1498-1499
If the configured bot token cannot create a Discord client and no webhook is available, `send` returns `Ok(None)` without posting. This method discards that result and returns `true`, so the watcher marks the threads as sent and does not retry them on later scans.

```suggestion
        if !self.has_delivery_path() {
            return Ok(false);
        }
        let _ = self.send(message).await?;
        Ok(true)
```

### Issue 3
crates/claudear-engine/src/support_digest.rs:75-76
If two digest instances use different database files in the same directory, both use `support_digest_sent.json`. Each overwrites the other's sent-thread history, so an instance can re-announce old threads after restart or suppress threads it has never announced.

### Issue 4
crates/claudear-engine/src/support_digest.rs:255-258
If a request for an older message page fails, `?` discards the newer pages already fetched and the caller skips the entire thread. A transient Discord error can therefore remove an active, urgent thread from that digest scan.

### Issue 5
crates/claudear-config/src/config.rs:770-775
Validation accepts any positive `days` value, but collection passes it to `chrono::Duration::days` and subtracts it from the current date. A sufficiently large configured value passes startup validation and then panics on every scheduled scan instead of producing a configuration error.

### Issue 6
crates/claudear-engine/src/housekeeping.rs:173-178
The spawned scan is neither joined nor drained when housekeeping stops. If shutdown occurs during a scan, runtime teardown can cancel it while it reads threads or posts a digest; a post whose sent-state update did not complete can then be announced again after restart.

### Issue 7
crates/claudear-integrations/src/reports/support.rs:181-186
If a poster says, “It works now; I’ll share the solution tomorrow,” the new follow-up check rejects the message before checking its explicit fix confirmation. The resolved thread is classified as needing a reply, taking a ranked digest slot and disappearing from the closure list.

### Issue 8
crates/claudear-engine/src/support_digest.rs:94-95
The new collection and triage paths have no behavioral tests for truncated histories, status changes, delivery failures, or configured entry limits. That leaves the decisions about which threads appear-and whether another notification is attempted-unguarded. Test those observable outcomes rather than mirroring regexes or configuration values.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 9
crates/claudear-integrations/src/telemetry.rs:289-292
A notifier can return `Ok(false)` when it did not post the digest, but this wrapper still logs “Notified support digest.” For example, the console notifier produces a success log while the watcher records that nothing was posted and will retry. That makes delivery logs misleading when diagnosing a missing digest.

```suggestion
            Ok(v) => {
                if v {
                    tracing::info!(component = self.inner.name(), "Notified support digest");
                }
                Ok(v)
            }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR adds an opt-in, report-only Discord support forum digest that collects and ranks threads, posts a Discord summary, and retains sent-list state across restarts.

  • The latest change extends solved-title matching to [SOLVE] and documents that prefix.

Reviews (4) · Last reviewed commit: "fix(reports): treat [SOLVE] titles as so..."

Comment thread crates/claudear-engine/src/watcher.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-engine/src/housekeeping.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-integrations/src/notifier/discord.rs
Comment thread crates/claudear-integrations/src/reports/support.rs
Comment thread crates/claudear-engine/src/watcher.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment on lines +76 to +77
/// are marked new.
pub async fn collect(&self) -> Result<SupportDigest> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Digest behavior lacks tests The new collection and triage paths have no behavioral tests for truncated histories, status changes, delivery failures, or configured entry limits. That leaves the decisions about which threads appear—and whether another notification is attempted—unguarded. Test those observable outcomes rather than mirroring regexes or configuration values.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/claudear-engine/src/support_digest.rs
Line: 76-77

Comment:
**Digest behavior lacks tests** The new collection and triage paths have no behavioral tests for truncated histories, status changes, delivery failures, or configured entry limits. That leaves the decisions about which threads appear—and whether another notification is attempted—unguarded. Test those observable outcomes rather than mirroring regexes or configuration values.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

notify_support_digest now returns whether a channel posted the digest,
so a failed Discord post no longer silences those threads. max_entries
is capped at the ten threads the Discord message lists, so threads cut
from the message are not marked sent either.
Comment thread crates/claudear-integrations/src/notifier/discord.rs
Comment thread crates/claudear-integrations/src/telemetry.rs
- Read up to 1000 messages per thread instead of only the newest 100.
- Run the scan as its own task so it never holds up housekeeping.
- Re-read team roles after 6 hours instead of caching them forever.
- Only read bare thanks as resolved in short messages, and never when the
  poster promises a next step.
- Validate interval_hours and days, and never panic on a bad interval.
- Keep the last sent digest in a file next to the database so a restart
  does not re-announce the same threads.
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-config/src/config.rs Outdated
Comment thread crates/claudear-engine/src/housekeeping.rs
Comment thread crates/claudear-integrations/src/reports/support.rs Outdated
Comment thread crates/claudear-integrations/src/reports/support.rs Outdated
The support digest now reads open threads, their messages and team
authors from the Appwrite project the threads bot syncs the Discord
forum into, over its REST API, instead of calling Discord. Claudear
needs no access to the forum, and nothing is posted to it; the digest
still goes only to the configured notifier channel.

- Config takes endpoint, project_id, database_id, an optional api_key
  and guild_id (for thread links) in place of the forum bot token and
  channel; team_roles are matched against the authors table.
- Drop the Discord client additions (archived-thread paging, member
  roles, forum tags) that only the digest used.
- Scan inside the housekeeping tick instead of a detached task.
- Bound days to 365, and key the sent-state file on the database file.
- Report a digest as posted only when Discord has a delivery path, and
  log success only when it was posted.
- A poster saying it is fixed counts as resolved even without replies;
  "will try" style follow-ups wait on the poster.
@HarshMN2345 HarshMN2345 changed the title feat(reports): hourly Discord support forum digest feat(reports): hourly support threads digest Oct 1, 2026

@hansi-codes hansi-codes 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.

🔵 Tier A · Looks good to merge. Summary

Comment thread crates/claudear-engine/src/housekeeping.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-engine/src/support_digest.rs Outdated
Comment thread crates/claudear-integrations/src/reports/support.rs Outdated
With `drafts` on, each digest scan asks the QA agent, grounded in code
search and indexed Discord discussions, to answer up to `max_drafts` of
the listed threads whose draft is missing or older than their latest
message. Answers go to the threads project's `drafts` table as
`pending`, keyed by thread id; approved drafts are left alone.

A new Support page lists pending and failed drafts with their
conversation. Admins can edit an answer and approve or reject it, which
updates the row; the threads project posts approved answers to the
forum. The digest mentions how many drafts wait for review.

ThreadsStore now holds all reads and writes to the threads project and
is shared by the digest scan and the dashboard API.
@HarshMN2345 HarshMN2345 changed the title feat(reports): hourly support threads digest feat(reports): support threads digest and reviewed answer drafts Oct 1, 2026

@hansi-codes hansi-codes 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.

🟡 Tier B · 3 blocking findings to address. Summary

Comment thread crates/claudear-engine/src/watcher.rs Outdated
Comment thread crates/claudear-engine/src/api/routes.rs
Comment thread dashboard/src/pages/support.tsx
Comment thread dashboard/src/pages/support.tsx Outdated
# Conflicts:
#	crates/claudear-engine/src/lib.rs
#	crates/claudear-engine/src/watcher.rs
…iewed

- A replacement draft is saved only over the revision it was written
  against, so a review made while the agent was answering is kept.
- Reviews carry the draft revision the admin saw; the API answers 409
  when it changed, or when the draft is already being posted.
- The Support page follows a rewritten draft when there are no local
  edits, flags it when there are, and shows a load error instead of an
  empty queue.
- Each thread's newest 100 messages are read newest first, instead of
  sharing a row cap across a batch of threads.
- A poster missing an author id on older rows is matched to their id
  on newer messages.
- "expected results" counts as resolved only when stated as an outcome.
- The scan runs as its own task again and holds a run claim, so it no
  longer delays housekeeping and shutdown waits for it.
- Drafts that are being posted are left alone, and editing an answer
  resets the threads project's send progress.
Threads longer than one page were ranked on their newest 100 messages
only, which could hide earlier team replies or the poster's first
report. Each thread is now paged newest first up to 1,000 messages.
A title like "[SOLVE] please help" can be an open request, and solved
titles hide the thread from the digest entirely. [SOLVED], [CLOSED],
[FIXED] and [RESOLVED] still count, as do the forum's solved tags.
Saving a regenerated draft and saving a review read the row, compared
its revision and then wrote it in a separate request, so a concurrent
write between the two could still be overwritten.

Writes are now conditional in Appwrite itself. A new draft is created,
which fails when the row already exists. A replacement or a review
updates the row with X-Appwrite-Timestamp set to the revision it was
based on, which Appwrite refuses with a 409 when the row was updated
after it. Upserts are no longer used, since they ignore that header.

@hansi-codes hansi-codes 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.

🔵 Tier A · Looks good to merge. Summary

…ample

An answer the threads project posts is the bot's message, so unless the bot
is in team_user_ids the thread reads as needing a reply and is drafted again.
Table tests for how triage classifies a thread (team answer vs. a
promise to look, fixed vs. still broken, follow-ups), the poster's
unanswered streak and bumps, impact ranking, and which titles count
as solved.

This branch has not been deployed

No deployments
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.

1 participant