Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0228637
refactor(dgw): give the gateway state the provisioner task store
irvingoujAtDevolution Oct 6, 2026
28e7a55
feat(dgw): run AI analysis as a provisioner task
irvingoujAtDevolution Oct 7, 2026
b1d9108
feat(dgw): add endpoints to start and read an AI analysis
irvingoujAtDevolution Oct 6, 2026
b639b20
fix(dgw): store provisioner task jobs before reporting them queued
irvingoujAtDevolution Oct 8, 2026
c3eecad
fix(dgw): keep the stored job of a provisioner task on restart
irvingoujAtDevolution Oct 8, 2026
c13af19
docs(dgw): add the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
0c6388d
docs(dgw): point the provisioner tasks intent to the task store crate
irvingoujAtDevolution Oct 8, 2026
995e70c
docs(dgw): add the jrec intent
irvingoujAtDevolution Oct 8, 2026
42292bd
docs(dgw): expand the provisioner tasks intent
irvingoujAtDevolution Oct 8, 2026
4367f20
test(dgw): keep the DVLS 2022.3 scope snapshot frozen
irvingoujAtDevolution Oct 13, 2026
61fc788
fix(dgw): keep the AI provider URL out of Debug output
irvingoujAtDevolution Oct 13, 2026
7b2804b
fix(dgw): bound the AI analysis progress percent in the OpenAPI schema
irvingoujAtDevolution Oct 13, 2026
df57948
fix(dgw): keep Gateway starting when provisioner tasks cannot be resumed
irvingoujAtDevolution Oct 13, 2026
c4e1032
style(dgw): separate items and group imports in the AI analysis code
irvingoujAtDevolution Oct 13, 2026
089ecaf
fix(dgw): ignore unknown fields in the AI analysis request
irvingoujAtDevolution Oct 13, 2026
38325bc
fix(dgw): start no AI analysis attempt that cannot finish before the …
irvingoujAtDevolution Oct 13, 2026
62f707b
refactor(dgw): keep only step, done and total in the AI analysis prog…
irvingoujAtDevolution Oct 13, 2026
3a8cbd3
fix(dgw): wait for the remux of the session before the AI analysis re…
irvingoujAtDevolution Oct 13, 2026
a6d4af9
fix(dgw): tie the AI analysis working files to their Task
irvingoujAtDevolution Oct 13, 2026
1803230
feat(dgw): start a new AI analysis Task of a recording from scratch
irvingoujAtDevolution Oct 13, 2026
3e66e69
style(dgw): format the AI analysis task tests
irvingoujAtDevolution Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/job-queue-libsql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ tracing = "0.1"
typed-builder = "0.21"
ulid = { version = "1.2", features = ["uuid"] }
uuid = "1.23"

[dev-dependencies]
tokio = { version = "1.52", features = ["macros", "rt"] }
73 changes: 73 additions & 0 deletions crates/job-queue-libsql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,27 @@ impl JobQueue for LibSqlJobQueue {

Ok(Some(scheduled_for))
}

async fn job_definitions(&self, name: &str) -> anyhow::Result<Vec<String>> {
let sql_query = "SELECT json(def) FROM job_queue WHERE name = :name AND failed_attempts < :max_attempts";

let params = ((":name", name), (":max_attempts", self.max_attempts));

trace!(%sql_query, ?params, "Listing job definitions");

let mut rows = self
.conn
.query(sql_query, params)
.await
.context("failed to execute SQL query")?;

let mut definitions = Vec::new();
while let Some(row) = rows.next().await.context("failed to read the row")? {
definitions.push(row.get::<String>(0).context("failed to read the job definition")?);
}

Ok(definitions)
}
}

// Typically, migrations should not be modified once released, and we should only be appending to this list.
Expand All @@ -425,3 +446,55 @@ const MIGRATIONS: &[&str] = &[

CREATE INDEX idx_scheduled_for ON job_queue(scheduled_for);",
];

#[cfg(test)]
mod tests {
use job_queue::{DynJob, Job, JobQueue as _, RunnerWaker};

use super::*;

struct Named {
name: &'static str,
def: &'static str,
}

#[async_trait]
impl Job for Named {
fn name(&self) -> &str {
self.name
}

fn write_json(&self) -> anyhow::Result<String> {
Ok(self.def.to_owned())
}

async fn run(&mut self) -> anyhow::Result<()> {
Ok(())
}
}

#[tokio::test]
async fn job_definitions_lists_the_stored_jobs_of_one_name() {
let conn = libsql::Builder::new_local(":memory:")
.build()
.await
.expect("build database")
.connect()
.expect("connect");
let queue = LibSqlJobQueue::builder()
.runner_waker(RunnerWaker::new(|| {}))
.conn(conn)
.build();
queue.setup().await.expect("setup");

for (name, def) in [("a", r#"{"n":1}"#), ("b", r#"{"n":2}"#), ("a", r#"{"n":3}"#)] {
let job: DynJob = Box::new(Named { name, def });
queue.push_job(&job, None).await.expect("push");
}

let mut definitions = queue.job_definitions("a").await.expect("list");
definitions.sort();

assert_eq!(definitions, [r#"{"n":1}"#, r#"{"n":3}"#]);
}
}
3 changes: 3 additions & 0 deletions crates/job-queue/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ pub trait JobQueue: Send + Sync {

/// Retrieves the closest future scheduled date
async fn next_scheduled_date(&self) -> anyhow::Result<Option<OffsetDateTime>>;

/// Lists the definitions of the stored jobs named `name` that may still run, in no particular order.
async fn job_definitions(&self, name: &str) -> anyhow::Result<Vec<String>>;
}

pub struct JobCtx {
Expand Down
4 changes: 4 additions & 0 deletions devolutions-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ transport.path = "../crates/transport"
jmux-proxy.path = "../crates/jmux-proxy"
devolutions-agent-shared.path = "../crates/devolutions-agent-shared"
devolutions-gateway-task.path = "../crates/devolutions-gateway-task"
devolutions-gateway-ai.path = "../crates/devolutions-gateway-ai"
devolutions-log.path = "../crates/devolutions-log"
job-queue.path = "../crates/job-queue"
job-queue-libsql.path = "../crates/job-queue-libsql"
Expand All @@ -38,6 +39,8 @@ sysevent-codes.path = "../crates/sysevent-codes"
agent-tunnel = { path = "../crates/agent-tunnel" }
agent-tunnel-libsql.path = "../crates/agent-tunnel-libsql"
gateway-db.path = "../crates/gateway-db"
provisioner-task.path = "../crates/provisioner-task"
provisioner-task-libsql.path = "../crates/provisioner-task-libsql"
ironrdp-pdu = { version = "0.9", features = ["std"] }
ironrdp-core = { version = "0.2", features = ["std"] }
ironrdp-rdcleanpath = "0.2"
Expand Down Expand Up @@ -91,6 +94,7 @@ tracing = "0.1"

# Async, futures…
tokio = { version = "1.52", features = ["signal", "net", "io-util", "time", "rt", "rt-multi-thread", "sync", "macros", "parking_lot", "fs"] }
tokio-util = "0.7"
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "json", "stream", "http2", "socks"] }
http-client-proxy = { path = "../crates/http-client-proxy" }
Expand Down
10 changes: 10 additions & 0 deletions devolutions-gateway/src/api/jrec.intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Background

Jrec is the endpoint to push, streaming, read recordings and its associated files.


# AI analysis

An AI analysis is a task initiated by the provisioner.
It requires the scope token with access `gateway.tasks.recording-ai-analysis.start` to access the url `POST /jet/jrec/{session_id}/ai-analysis`; Naturally in order to read the task's state, it requires the scope token with access `gateway.tasks.recording-ai-analysis.read`.
The analyzed output belongs to the recording reading scope, however, the associated metadata, including but not limited to error messages, token usages etc. belong to the task reading scope.
Loading
Loading