Repository navigation
feat(dgw): analyze any recording in the AI crate - #2043
Open
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 18 commits into
Open
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 18 commits into
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 18 commits into
Conversation
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2045
October 6, 2026 21:02
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2045
October 6, 2026 21:26
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 6, 2026 21:27
b11c4b1 to
326f4bb
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2046
October 6, 2026 21:27
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 7, 2026 17:22
326f4bb to
1eb2c97
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
2 times, most recently
from
October 7, 2026 17:50
f546209 to
e1d6488
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 7, 2026 20:12
e1d6488 to
c77eec2
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2046
October 7, 2026 20:36
irvingouj@Devolutions (irvingoujAtDevolution)
changed the base branch from
feat/ai-analysis-plumbing
to
feat/provisioner-task
October 7, 2026 20:36
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2049
October 7, 2026 20:36
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 7, 2026 20:36
c77eec2 to
63fff82
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 7, 2026 21:13
63fff82 to
c2dc1c7
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2049
October 7, 2026 22:19
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 7, 2026 22:19
c2dc1c7 to
36073f9
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
changed the base branch from
feat/provisioner-task
to
feat/ai-analysis-plumbing
October 7, 2026 22:19
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2051
October 7, 2026 22:19
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2051
October 7, 2026 22:20
irvingouj@Devolutions (irvingoujAtDevolution)
changed the base branch from
feat/ai-analysis-plumbing
to
feat/provisioner-task
October 7, 2026 22:20
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2052
October 7, 2026 22:20
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2052
October 8, 2026 19:35
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2059
October 8, 2026 19:35
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
3 times, most recently
from
October 13, 2026 16:06
afdd351 to
2c30aa5
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
added a commit
that referenced
this pull request
Oct 13, 2026
Gateway can now describe what the user did in a recorded session with an AI provider, and add the result to the session as a new log. The analysis itself is the AI crate's `analyze_recording`; Gateway gives it the session manifest once the recording is finished, then adds the log it returns to the session through the recording manager. Provisioner tasks run through a ProvisionerTaskRunner, the one place that keeps a task's record in gateway.db and the job running it on the job queue in step. Each kind of work is a ProvisionerTask: what one attempt does, and what to drop once its record ends. The runner records the Task and queues its job, starts each attempt, cancels and awaits an attempt past its timeout, retries transient failures until the job queue's attempts are used up, and records how the Task ends. The Task is the source of truth and its job only wakes it up: each Task names the one job allowed to run it, so a stale or duplicate job stops without running anything. At startup, before the job runner claims anything, every unfinished Task is handed to a new job, which also covers a Task recorded just before a crash, before its job was queued. AI analysis (`recording-ai-analysis`) is the first kind. A repeated request with the same Task ID gets its Task back, even while the session records again. A new request is refused while the session is recording, or when it has no recording. The API key and the base URL stay in memory only, encrypted, as the Task's secret in the provisioning store, until the Task ends or reaches its deadline. After a restart the attempt finds no key and fails its Task. Neither the key nor the URL appears in errors, logs or stored results: the AI client (#2043) leaves the URL out of its transport errors. Analyses of one session run one at a time, so two attempts never share the working files the AI crate keeps next to the manifest. Those files are removed when the Task ends. Starting an AI analysis is an internal function for now; the HTTP endpoint comes next. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 13, 2026 17:01
2c30aa5 to
a174f27
Compare
A finished recording can now be read as asciicast lines without tailing it. The lines come out one packet at a time, so memory use does not grow with the recording. The packet decoding is shared with the live stream decoder. Events written before the terminal setup are still held until the setup arrives, but no more than 1 MiB of them: past that, the header goes out with the terminal size known so far, so a recording that never sends its setup is not held whole in memory. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Action times now come from the input, never from the model. Each transcript line sent to `describe_session_actions` gets an id (`L12`), the model cites the line where an action starts, and the action takes the time of that line. Actions citing a line that is not in the transcript are skipped. The prompt version is now `session-actions-3`. A new purpose, `describe_screen_actions`, lists what the user did in screenshots of a session: each PNG is sent after a label with its id and time, the model cites the screenshot that shows each action, and the action takes its time. Screenshots marked as context show where the session was and are never cited. Requests can now carry images, for OpenAI chat completions and Anthropic Messages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`AiClient::analyze_recording` takes the parsed session manifest and its path and does the rest. A session is recorded either as a terminal or as a video, and the types hold it: `Recordings` is all terminal or all video, and each recording is accepted only with the extension of its kind (`Recordings::from_files` refuses a manifest that mixes them or has none). - terminal recordings (asciicast and TRP) are read as a stream into timed transcript chunks, described with `describe_session_actions`; - video recordings (WebM, VP8 or VP9) are decoded with XMF and sampled twice a second; a screenshot is kept when the screen changed, once it settles, and every 3 seconds during a long change (16x16 tiles of a half-resolution gray image; tiles that keep changing, such as a clock, are ignored). The screenshots are sent at most 1280x720, 40 per request, with the previous one as context, and described with `describe_screen_actions`. A truncated answer is asked again in two halves. Each chunk's answer is saved as a checkpoint, so analysing again after a failure only asks about the chunks left. The actions of all chunks are merged in time order into a Session Recording Log, left next to the manifest. The working files live in `.ai-analysis/` next to the manifest; files made with another model or prompt are not reused. A successful analysis removes them, and `recording_analysis::discard` removes them otherwise. The crate never writes `recording.json`. `start` runs the analysis on the Tokio runtime and returns a channel of events: `Progress` (reading the recordings, with the bytes read; asking the AI, chunk by chunk; writing the log), then `Complete` or `Failure`, last. Each `Progress` has a stable stage name, a percent and a short sentence, to be stored with a task or shown to whoever asks about it. Dropping the receiver cancels the analysis. The `analyze_recording` example runs it against a real provider and shows the progress: `cargo run -p devolutions-gateway-ai --example analyze_recording -- --recording-path <path>`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every CodecID was taken as the video codec, so a WebM with an audio track, such as VP8 video with Opus audio, was refused as an unsupported video codec, and blocks were decoded whatever their track. The first video track is now picked from the track entries, and only its blocks are decoded; audio and other tracks are skipped. Checked locally with XMF on a real session remuxed with an Opus track before and after the video: the same 32 screenshots as the original.
The first reading progress came from an interval tick racing the end of the reading, so a small recording could be read before any progress was sent. The start of the reading is now reported before the files are read.
reqwest errors repeat the URL of the request, so a transport error named the provider's endpoint, which callers store in Task results and logs. Transport errors now drop the URL, and the request debug log no longer names the base URL.
The WebM parser zero-fills the size an element claims before reading it, up to 4 GB by default, so a small hostile recording could claim gigabytes. Elements larger than the file are now refused. The cap is the file length, not a fixed constant, because remuxed recordings have a known Segment size as large as the file and the cap applies to every element. Decoded frames larger than 8192x8192 are refused before they are copied. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A frame that stays on screen for a long time, as in an idle session, is sampled many times. Each sample now checks the cancellation token, and the half-resolution gray image of a frame is computed on its first sample only, instead of on every sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Events held until the terminal setup were counted by payload size only, so a flood of empty packets was held whole in memory. Each packet now counts its 8-byte header too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
VpxError displays only "decoder error"; the libvpx code is in its source. The messages now show the Display chain instead of the Debug output. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The library logs its steps at debug; the caller decides what is worth an info line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The size of the recordings is read on a blocking thread, and the check for a chunk already described uses tokio::fs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o pauses - A panic in the analysis left the watcher holding a sender, so the events channel never ended and the caller waited forever. The watcher now stops through a drop guard, so the channel ends as documented. - Dropping the receiver cancelled the caller's own token; it now cancels a child token, so a shared token is never cancelled for other work. - A pause between two video frames was sampled every half second, so a corrupt timestamp far ahead spun until the attempt timeout. Past the animation window one picture no longer changes the frame selector, so the rest of the pause is skipped and counted, with the same frame IDs. - PNG frames are finished explicitly, so a write or flush error is reported instead of dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The working files next to a recording were reused by any later analysis with the same model and prompt version, and `discard` removed them whoever made them. A caller that runs several analyses of one recording, such as one Task after another, could resume from another one's checkpoints or delete the checkpoints of one that is still retrying. `AnalyzeRecording::owner` records whose the files are: only an analysis of the same owner resumes from them, and another owner starts fresh. `discard(manifest_path, Some(owner))` leaves another owner's files in place; `discard(manifest_path, None)` removes them whoever made them, for a caller that starts over from scratch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
irvingouj@Devolutions (irvingoujAtDevolution)
force-pushed
the
feat/ai-analysis-pipeline
branch
from
October 13, 2026 17:21
a174f27 to
cfa0e8c
Compare
irvingouj@Devolutions (irvingoujAtDevolution)
removed this pull request from stack #2059
October 13, 2026 17:32
irvingouj@Devolutions (irvingoujAtDevolution)
changed the base branch from
feat/provisioner-task
to
build/cadeau-0.5.3
October 13, 2026 17:32
irvingouj@Devolutions (irvingoujAtDevolution)
added this pull request to stack #2073
October 13, 2026 17:32
irvingouj@Devolutions (irvingoujAtDevolution)
requested a review
from Benoît Cortier (CBenoit)
October 13, 2026 18:19
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AI analysis of any recorded session, as one call in the AI crate:
AiClient::analyze_recording(&manifest, &manifest_path)takes the parsed session manifest and its path, and does the rest..cast,.trp): streamed into timed transcript chunks, described withdescribe_session_actions..webm, VP8/VP9): decoded with XMF, sampled twice a second, and a screenshot is kept where the screen changed (once it settles, and every 3 s during a long change; tiles that keep changing, like a clock, are ignored). Screenshots go out at most 1280x720, 40 per request, described by the newdescribe_screen_actionspurpose. This is the frames-first design from the eval (no OCR).L12) and screenshots get ids (F00012); the model cites one, and the action takes its time..ai-analysis/next to the manifest (not reused across models or prompts); the finished log is left as.ai-analysis.slogfor the caller to move. The crate never writesrecording.json.The manifest type is the crate's own (
RecordingManifest), so Gateway'sJrecManifeststays out of the crate's API.Also in this PR
terminal-streamerdecodes a complete.trprecording as a stream (was feat(dgw): analyze TRP recordings with AI #2040).session_actionsprompt is nowsession-actions-3(cites lines).Where to look hardest
recording_analysis/video.rs: frame selection and the decode loop (onenext_frame()per decoded frame)recording_analysis/mod.rs:prepare(plan, reuse of working files) andsend(resume, cancellation)Tested
devolutions-gateway-ai91 + 28: frame selector on synthetic screens, screenshot requests for OpenAI and Anthropic (image blocks, labels, context not citable), line citation, resume,discard, cancellation.analyze_recordingexample:.castsession (3 min oftop) and a.trpsession (6 min ofhtop): one action each, correct.🤖 Generated with Claude Code