Skip to content

Comments

feat(imessage): implement Phase 5 daemon command handlers#8

Merged
wolfiesch merged 1 commit intomasterfrom
feat/phase5-daemon-handlers
Jan 11, 2026
Merged

feat(imessage): implement Phase 5 daemon command handlers#8
wolfiesch merged 1 commit intomasterfrom
feat/phase5-daemon-handlers

Conversation

@wolfiesch
Copy link
Owner

Summary

  • Implement all 8 command handlers in DaemonService connecting hot resources (SQLite connection + contacts cache) to query logic
  • Add shared db::helpers module for reusable query functions
  • Refactor analytics.rs to use shared helpers

Commands Implemented

Command Time Description
recent 2.4ms List recent messages with contact enrichment
unread 2.8ms List unread messages
analytics 20.5ms Message stats, busiest times, top contacts
followup 6.1ms Unanswered questions and stale conversations
handles 3.0ms List all message handles
unknown - Filter handles not in contacts
discover - Find frequent unknown senders as contact candidates
bundle - Combine multiple queries in single request

Performance

5/8 commands meet sub-5ms target. Analytics is slower (20ms) due to 6 sequential queries - future optimization opportunity.

Test plan

  • Build passes: cargo build --release
  • All commands return valid JSON via wolfies-imessage-client
  • Benchmarked with hyperfine

🤖 Generated with Claude Code

Implement all 8 command handlers in DaemonService to connect hot
resources (SQLite connection + contacts cache) to query logic:

- recent: 2.4ms - list recent messages with contact enrichment
- unread: 2.8ms - list unread messages
- analytics: 20.5ms - message stats, busiest times, top contacts
- followup: 6.1ms - unanswered questions and stale conversations
- handles: 3.0ms - list all message handles
- unknown: filter handles not in contacts
- discover: find frequent unknown senders as contact candidates
- bundle: combine multiple queries in single request

Key changes:
- Add src/db/helpers.rs with shared query functions
- Refactor analytics.rs to use db::helpers
- Implement all handlers in daemon/service.rs

Performance: 5/8 commands meet sub-5ms target. Analytics slower
due to 6 sequential queries (future optimization opportunity).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 11, 2026 09:21
@vercel
Copy link

vercel bot commented Jan 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
life-planner-api Ready Ready Preview, Comment Jan 11, 2026 9:21am

@gemini-code-assist
Copy link

Summary of Changes

Hello @wolfiesch, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly advances the wolfies-imessage daemon by implementing all remaining Phase 5 command handlers. It centralizes database query logic into a new db::helpers module, which is then utilized by the daemon's services and refactored existing commands like analytics. This ensures efficient data retrieval from the SQLite database, leveraging hot resources and contact caching to provide comprehensive iMessage data analysis and management capabilities.

Highlights

  • Daemon Command Handlers Implemented: All eight planned command handlers (recent, unread, analytics, followup, handles, unknown, discover, bundle) have been fully implemented within the DaemonService, completing a major phase of functionality.
  • Shared Database Helpers: A new db::helpers module was introduced to centralize and share database query logic, improving code reusability and maintainability across CLI commands and the daemon service.
  • Analytics Refactoring: The analytics.rs command was refactored to utilize the new shared db::helpers module, removing previously duplicated query logic and streamlining its implementation.
  • Performance Considerations: While 5 out of 8 commands meet the sub-5ms performance target, the analytics command currently takes 20ms due to sequential query execution within the daemon, identified as a future optimization opportunity.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request is a significant step forward, implementing the daemon command handlers and refactoring query logic into a shared db::helpers module. The refactoring to centralize database logic is a great improvement for maintainability. My review focuses on further improving the new code by reducing duplication and increasing robustness. I've identified several areas in daemon/service.rs where repeated logic for parameter parsing and data enrichment can be extracted into helper methods. The bundle handler, in particular, has a high degree of code duplication that should be addressed. In the new db/helpers.rs file, I've suggested moving a hardcoded SQL query to the central queries.rs file for consistency and pointed out a potential panic that could affect the daemon's stability. A key finding is that while the analytics command was refactored, the followup command in commands/analytics.rs was not, leaving a large amount of duplicated code that should also be updated to use the new helpers.

Comment on lines +401 to 491
fn bundle(&self, params: HashMap<String, serde_json::Value>) -> Result<serde_json::Value> {
let include = params
.get("include")
.and_then(|v| v.as_str())
.unwrap_or("unread_count,recent");

let sections: Vec<&str> = include.split(',').map(|s| s.trim()).collect();
let mut result = serde_json::Map::new();

for section in sections {
match section {
"unread_count" => {
let unread = helpers::query_unread_messages(&self.conn, 100)?;
result.insert("unread_count".to_string(), serde_json::json!(unread.len()));
}
"recent" => {
let limit = params
.get("recent_limit")
.and_then(|v| v.as_u64())
.unwrap_or(10) as u32;
let days = params
.get("recent_days")
.and_then(|v| v.as_u64())
.unwrap_or(7) as u32;
let cutoff = queries::days_ago_cocoa(days);
let messages = helpers::query_recent_messages(&self.conn, cutoff, limit)?;

let enriched: Vec<serde_json::Value> = messages
.into_iter()
.map(|msg| {
let name = self.contacts.find_by_phone(&msg.phone).map(|c| c.name.clone());
serde_json::json!({
"text": msg.text,
"date": msg.date,
"is_from_me": msg.is_from_me,
"phone": msg.phone,
"contact_name": name,
})
})
.collect();
result.insert("recent".to_string(), serde_json::json!(enriched));
}
"analytics" => {
let days = params
.get("analytics_days")
.and_then(|v| v.as_u64())
.unwrap_or(30) as u32;
let cutoff = queries::days_ago_cocoa(days);

let (total, sent, received) =
helpers::query_message_counts(&self.conn, cutoff, None)?;

result.insert(
"analytics".to_string(),
serde_json::json!({
"total_messages": total,
"sent_count": sent,
"received_count": received,
"period_days": days,
}),
);
}
"followup_count" => {
let days = params
.get("followup_days")
.and_then(|v| v.as_u64())
.unwrap_or(30) as u32;
let stale = params
.get("followup_stale")
.and_then(|v| v.as_u64())
.unwrap_or(3) as u32;

let cutoff = queries::days_ago_cocoa(days);
let stale_ns = (stale as i64) * 24 * 3600 * 1_000_000_000;

let unanswered = helpers::query_unanswered_questions(&self.conn, cutoff, stale_ns)?;
let stale_convos = helpers::query_stale_conversations(&self.conn, cutoff, stale_ns)?;

result.insert(
"followup_count".to_string(),
serde_json::json!(unanswered.len() + stale_convos.len()),
);
}
_ => {
// Unknown section, skip silently
}
}
}

Ok(serde_json::Value::Object(result))
}

Choose a reason for hiding this comment

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

high

The bundle handler duplicates a significant amount of logic from other handlers like recent, analytics, and followup. This creates a major maintainability issue, as any change in the logic of those handlers needs to be manually synchronized here. The bundle handler should be refactored to reuse the logic from the other handlers, for example by calling them directly with appropriate parameters, or by extracting the shared logic (parameter parsing, querying, and enrichment) into private helper methods that all handlers can use.

Comment on lines +84 to +91
let days = params
.get("days")
.and_then(|v| v.as_u64())
.unwrap_or(7) as u32;
let limit = params
.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(20) as u32;

Choose a reason for hiding this comment

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

medium

The logic for parsing optional u32 parameters from the params HashMap with a default value is repeated in almost every command handler (recent, unread, analytics, followup, etc.). This adds boilerplate and is prone to inconsistencies. Consider extracting this into a private helper function to improve code reuse and readability.

For example, you could add a helper method to DaemonService:

fn get_param_u32(&self, params: &HashMap<String, serde_json::Value>, key: &str, default: u32) -> u32 {
    params
        .get(key)
        .and_then(|v| v.as_u64())
        .map(|v| v as u32)
        .unwrap_or(default)
}

And then use it like this:

let days = self.get_param_u32(&params, "days", 7);
let limit = self.get_param_u32(&params, "limit", 20);

Comment on lines +97 to +112
let enriched: Vec<serde_json::Value> = messages
.into_iter()
.map(|msg| {
let contact_name = self
.contacts
.find_by_phone(&msg.phone)
.map(|c| c.name.clone());
serde_json::json!({
"text": msg.text,
"date": msg.date,
"is_from_me": msg.is_from_me,
"phone": msg.phone,
"contact_name": contact_name,
})
})
.collect();

Choose a reason for hiding this comment

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

medium

The logic to enrich data with contact names is duplicated across multiple handlers (recent, unread, followup, handles, bundle). This is a lot of repeated code that could lead to maintenance issues. You should extract this into private helper methods on DaemonService for each data type to improve code reuse and maintainability.

For example, for RecentMessage:

// In `impl DaemonService`
fn enrich_recent_message(&self, msg: helpers::RecentMessage) -> serde_json::Value {
    let contact_name = self.contacts.find_by_phone(&msg.phone).map(|c| c.name.clone());
    serde_json::json!({
        "text": msg.text,
        "date": msg.date,
        "is_from_me": msg.is_from_me,
        "phone": msg.phone,
        "contact_name": contact_name,
    })
}

Then you can simplify the collection mapping:

let enriched: Vec<serde_json::Value> = messages
    .into_iter()
    .map(|msg| self.enrich_recent_message(msg))
    .collect();

.unwrap_or(3) as u32;

let cutoff_cocoa = queries::days_ago_cocoa(days);
let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000;

Choose a reason for hiding this comment

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

medium

The calculation (stale as i64) * 24 * 3600 * 1_000_000_000 uses magic numbers for time conversion. This makes the code harder to read and understand. It's better to define constants like SECONDS_IN_DAY and NANOS_IN_SECOND to make the calculation self-documenting. This calculation is also repeated in the bundle handler (line 474).

Suggested change
let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000;
let stale_threshold_ns = (stale as i64) * 24 * 3600 * 1_000_000_000; // TODO: Use constants for time units

Comment on lines +227 to +242
let mut stmt = conn.prepare(
r#"
SELECT
m.text,
m.date,
m.is_from_me,
h.id as handle
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE m.date >= ?1
AND (m.associated_message_type IS NULL OR m.associated_message_type = 0)
AND m.text IS NOT NULL
ORDER BY m.date DESC
LIMIT ?2
"#,
)?;

Choose a reason for hiding this comment

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

medium

The SQL query for recent messages is hardcoded inside the query_recent_messages function. For consistency with other queries in the project, this should be extracted into a constant in src/db/queries.rs. This improves maintainability by keeping all SQL queries in one place.

Comment on lines +393 to +395
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")

Choose a reason for hiding this comment

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

medium

The use of .expect("Time went backwards") could cause the daemon to panic if the system clock is adjusted backwards. While this is a rare event, for a long-running service, it's better to handle this case gracefully instead of crashing. Consider changing this function to return a Result or handle the Err case from duration_since without panicking, for example by returning the current time as a fallback.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements Phase 5 of the iMessage daemon, completing all 8 command handlers that leverage hot resources (persistent SQLite connection and cached contacts) for sub-5ms query performance. The implementation introduces a shared db::helpers module containing reusable query functions, and refactors the existing analytics.rs CLI command to use these shared helpers, eliminating code duplication.

Changes:

  • Added db::helpers module with 9 query helper functions and 7 data structures for analytics, reading, discovery, and follow-up queries
  • Implemented 8 daemon command handlers (recent, unread, analytics, followup, handles, unknown, discover, bundle) in DaemonService
  • Refactored analytics.rs CLI command to use shared helpers, removing 90+ lines of duplicate query code

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/lib.rs Added CHANGELOG entry for db::helpers module
src/db/mod.rs Added helpers module export and alphabetized module declarations
src/db/helpers.rs New shared query helpers module with data structures, query functions, and utility functions for timestamp conversion
src/daemon/service.rs Implemented all 8 command handlers with contact enrichment and hot connection reuse
src/commands/analytics.rs Refactored to use shared helpers from db::helpers module

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +227 to +242
let mut stmt = conn.prepare(
r#"
SELECT
m.text,
m.date,
m.is_from_me,
h.id as handle
FROM message m
LEFT JOIN handle h ON m.handle_id = h.ROWID
WHERE m.date >= ?1
AND (m.associated_message_type IS NULL OR m.associated_message_type = 0)
AND m.text IS NOT NULL
ORDER BY m.date DESC
LIMIT ?2
"#,
)?;
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

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

The query_recent_messages function uses an inline SQL query string instead of referencing a constant from the queries module like other helper functions do (e.g., query_unread_messages uses queries::UNREAD_MESSAGES). For consistency and maintainability, consider extracting this SQL into a constant in queries.rs and referencing it here, similar to how other query helpers are implemented.

Copilot uses AI. Check for mistakes.
@wolfiesch wolfiesch merged commit 8b8ff91 into master Jan 11, 2026
8 of 9 checks passed
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