remem-ai 0.6.3

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use anyhow::Result;
use rusqlite::{params, Connection, OptionalExtension};

use crate::db::job::{Job, JobType};

pub fn claim_next_job(
    conn: &mut Connection,
    lease_owner: &str,
    lease_secs: i64,
) -> Result<Option<Job>> {
    let now = chrono::Utc::now().timestamp();
    let lease_expires = now + lease_secs.max(1);
    let tx = conn.transaction()?;
    let candidate: Option<i64> = tx
        .query_row(
            "SELECT candidate.id FROM jobs AS candidate
             WHERE candidate.state = 'pending'
               AND candidate.next_retry_epoch <= ?1
               AND NOT (
                   candidate.job_type = 'compile_rules'
                   AND EXISTS (
                       SELECT 1 FROM jobs AS predecessor
                       WHERE predecessor.job_type = 'compile_rules'
                         AND predecessor.project = candidate.project
                         AND predecessor.state = 'processing'
                   )
               )
             ORDER BY candidate.priority ASC,
                      candidate.created_at_epoch ASC,
                      candidate.id ASC
             LIMIT 1",
            params![now],
            |row| row.get(0),
        )
        .optional()?;

    let Some(job_id) = candidate else {
        tx.commit()?;
        return Ok(None);
    };

    let updated = tx.execute(
        "UPDATE jobs AS candidate
         SET state = 'processing',
             lease_owner = ?1,
             lease_expires_epoch = ?2,
             updated_at_epoch = ?3
         WHERE candidate.id = ?4
           AND candidate.state = 'pending'
           AND NOT (
               candidate.job_type = 'compile_rules'
               AND EXISTS (
                   SELECT 1 FROM jobs AS predecessor
                   WHERE predecessor.job_type = 'compile_rules'
                     AND predecessor.project = candidate.project
                     AND predecessor.state = 'processing'
               )
           )",
        params![lease_owner, lease_expires, now, job_id],
    )?;
    if updated == 0 {
        tx.commit()?;
        return Ok(None);
    }

    let job = load_claimed_job(&tx, job_id)?;
    tx.commit()?;
    Ok(Some(job))
}

fn load_claimed_job(conn: &Connection, job_id: i64) -> Result<Job> {
    let row = conn.query_row(
        "SELECT id, host, job_type, project, session_id, payload_json, attempt_count, max_attempts
         FROM jobs WHERE id = ?1",
        params![job_id],
        |row| {
            Ok((
                row.get::<_, i64>(0)?,
                row.get::<_, String>(1)?,
                row.get::<_, String>(2)?,
                row.get::<_, String>(3)?,
                row.get::<_, Option<String>>(4)?,
                row.get::<_, String>(5)?,
                row.get::<_, i64>(6)?,
                row.get::<_, i64>(7)?,
            ))
        },
    )?;

    Ok(Job {
        id: row.0,
        host: row.1,
        job_type: JobType::from_db(&row.2)?,
        project: row.3,
        session_id: row.4,
        payload_json: row.5,
        attempt_count: row.6,
        max_attempts: row.7,
    })
}

#[cfg(test)]
mod eligibility_tests {
    use rusqlite::{params, Connection};

    use super::claim_next_job;
    use crate::db::{enqueue_job, JobType};
    use crate::migrate::MIGRATIONS;

    fn setup_conn() -> Connection {
        let conn = Connection::open_in_memory().expect("in-memory db should open");
        for migration in MIGRATIONS {
            conn.execute_batch(migration.sql)
                .expect("schema migration should load");
        }
        conn
    }

    fn compile_rules_with_successor(conn: &Connection) -> (i64, i64) {
        let source = enqueue_job(
            conn,
            "worker",
            JobType::CompileRules,
            "compile-project",
            None,
            "{}",
            1,
        )
        .expect("CompileRules source should enqueue");
        conn.execute(
            "UPDATE jobs SET state = 'processing', lease_owner = 'worker-a',
                 lease_expires_epoch = ?2 WHERE id = ?1",
            params![source, chrono::Utc::now().timestamp() + 60],
        )
        .expect("CompileRules source should enter processing");
        let successor = enqueue_job(
            conn,
            "worker",
            JobType::CompileRules,
            "compile-project",
            None,
            "{}",
            1,
        )
        .expect("CompileRules successor should enqueue");
        (source, successor)
    }

    #[test]
    fn claim_next_job_skips_compile_rules_successor_while_predecessor_processing() {
        let mut conn = setup_conn();
        let (_, successor) = compile_rules_with_successor(&conn);

        let claimed =
            claim_next_job(&mut conn, "worker-b", 60).expect("claim query should succeed");

        assert!(claimed.is_none());
        let state: String = conn
            .query_row(
                "SELECT state FROM jobs WHERE id = ?1",
                params![successor],
                |row| row.get(0),
            )
            .expect("successor state should load");
        assert_eq!(state, "pending");
    }

    #[test]
    fn claim_next_job_continues_to_unrelated_eligible_job() {
        let mut conn = setup_conn();
        compile_rules_with_successor(&conn);
        let ordinary = enqueue_job(
            &conn,
            "codex-cli",
            JobType::Compress,
            "ordinary-project",
            None,
            "{}",
            2,
        )
        .expect("ordinary job should enqueue");

        let claimed = claim_next_job(&mut conn, "worker-b", 60)
            .expect("claim query should succeed")
            .expect("unrelated job should remain eligible");

        assert_eq!(claimed.id, ordinary);
    }
}