Repository navigation
feat(dgw): run AI analysis of a recording as a provisioner task - #2044
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 53 commits into
Conversation
Let maintainers know that an action is required on their side
|
c81d3f1 to
5b16e3a
Compare
5b16e3a to
faf9419
Compare
faf9419 to
55fef54
Compare
55fef54 to
5ff453b
Compare
5ff453b to
f2948c9
Compare
f2948c9 to
633c904
Compare
633c904 to
6083262
Compare
6083262 to
ed522eb
Compare
The tests move from the crate's own `tests/` to the testsuite, as AGENTS.md asks, and talk to a running Gateway and a mock AI provider instead of linking Gateway internals. They read the key leak checks from the log files and `gateway.db` in the config folder. The check that a Task of another kind is not found at this endpoint is gone: the binary only knows one kind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st does The full path made the OpenAPI document reference a schema that does not exist. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…a failed removal A session without recording has no AI analysis to remove, so clearing it succeeds and the attempt then reports "session has no recording". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Windows can't delete the database and log files a running Gateway holds open, so each test left its folder behind. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er accepts The server reads a `u32`, which an `int32` schema can't hold, so the clients now get a 64-bit integer from 0 to 4294967295. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The AI access of a Task now lives in the standalone `SecretStore`, keyed by the Task ID, instead of the provisioning store, which is about sessions only. Gateway holds one store in `DgwState`, hands it to the provisioner tasks, and runs its expiry cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task secrets live in the standalone `SecretStore`, not in the provisioning store, which is about sessions only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Record the owner's rulings from the intent review of the AI analysis Task: a new Task replaces the earlier analysis only once its success is recorded, a failed Task leaves no file behind, attempt errors are recoverable or permanent, token usage is always reported, a running Task fails after 30 minutes without progress, and the base URL is a secret like the API key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Task keeps its API key until its deadline, and that deadline now moves while the Task makes progress, so the key must follow it. An expired or removed secret stays gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Errors: an attempt fails with a closed kind and no text, recoverable
or permanent. A failed Task's payload is `{ error, usage }`, so the
caller words the error.
Usage: every payload but `queued` carries the tokens used so far, over
all attempts. Requests whose usage the provider did not report are
counted apart in `unreportedRequests`.
Time: a Task fails if it does not start within 2 hours. Once it runs,
each attempt start and each progress report moves its deadline 30
minutes ahead, and the runner cancels an attempt past it. Waiting for
a remux is not progress. The store fails an overdue Task with
`deadlinePassed` and keeps the usage of its last payload.
Success: the new log sits in the session unlisted until the success is
recorded. Ending the Task then lists it and drops the earlier analysis.
If Gateway stops in between, the job runs again at startup and redoes
that step. A failed Task removes everything it made and leaves the
earlier analysis in place.
The session manifest is written to a file of its own, then renamed
over the old one, so a crash never leaves half of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The end of a Task, such as listing its AI analysis log, ran from the Task's own job, whose runs are shared with the attempts. Once those runs were used up, a failed end was never done again, not even at the next start. A failed end now hands the Task to a new job that ends it again five minutes later. The job is stored, so a restart runs it too. Deadlines are also cut to whole seconds, as the store keeps them, so the runner and the store agree on when one passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request was counted only once its answer came. One cut off by the deadline or a restart was never counted, so the usage looked complete when it was not. Each request now counts as unreported from the moment it is sent, and its reported tokens replace that count on answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Task whose end failed could list its log later, over the analysis of a newer Task of the session. A new Task now removes the logs earlier Tasks left unlisted, so such a late end finds nothing to list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manifest is written to a temporary file first. Its name now starts with a dot, which jrec never serves, so a file a crash leaves behind is never downloaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAPI document writes the error kinds out by hand, since utoipa drops the descriptions of variants without fields. An exhaustive match now stops the build until a new kind is documented too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new Task removed every log earlier Tasks left unlisted. When the end of a succeeded Task was still due, the new Task dropped an analysis whose success was recorded, and a new Task that then failed left the session without it. A new Task now does what the end of the earlier Task would: it lists the log of a succeeded Task, and removes the log of a failed or unknown one. The runner gives an attempt the state of another record for this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the success of a Task could not be recorded, any error that came after the failure was recorded was only logged. An end that could not be queued again was then lost. That error now reaches the job queue, whose retry finds the Task finished and only ends it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When neither the success nor the failure of an attempt could be recorded, the job ended and nothing ended the Task, so its log stayed on disk. A job now runs just after the deadline, when the store has failed the Task, and only ends it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ID of an unlisted log comes from a file name, so a Task of another session now reads as unknown, and its log is removed instead of listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Intent drafts: handoffThe interview read this branch at a4e8a98, before the rebase onto master. The intent it produced is in f231c48. ProvenanceLevels: 1 = my words verbatim, 2 = my words tidied, 3 = agent wording I adopted, 4 = agent fill-in, not intent.
Rows 9, 13, 20 and 21 were worded after the interview and shown to me together before the code work started. Agent fill-in (level 4, not in the intent files): 5 attempts as the retry limit, a failed end retried every 5 minutes with no cap, the unlisted log name Left out on purpose
Placement
Pending experimentsNone. Note Human-tuned, LLM-assisted content. |
| - `Running` -> `Failed` | ||
|
|
||
| Each Task has a deadline counted from when Gateway receives it. | ||
| Each Task has a deadline. While the Task runs, its deadline may move forward each time it makes progress. |
There was a problem hiding this comment.
| Each Task has a deadline. While the Task runs, its deadline may move forward each time it makes progress. | |
| Each Task has a deadline. | |
| While the Task runs, its deadline may move forward each time it makes progress. |
I would like a rationale added to this new line ^
AI analysis of a recording, started by DVLS with a plain HTTP request and run as a provisioner Task on the existing job queue.
ProvisionerTaskRunneris the one place that keeps a Task's record (#2041) and its job in step. Each kind of work is aProvisionerTask: what one attempt does, its time limits and payloads, and what to finish once the Task ends. The record is the source of truth and the job only wakes it up, so a stale or duplicate job does nothing. At startup, an unfinished Task left without a job gets a new one.API (stable, no
enable_unstable)POST /jet/jrec/{session_id}/ai-analysis, scopegateway.tasks.recording-ai-analysis.start. Body:taskId,provider,model,baseUrl(needed for openai-compatible),maxOutputTokens,language,apiKey.task_id_conflict/recording_active/analysis_in_progress(+ itstaskId)GET /jet/jrec/{session_id}/ai-analysis/{task_id}, scopegateway.tasks.recording-ai-analysis.read: the Task, or 404.The two scopes in
Devolutions.Gateway.Utilsand the regenerated OpenAPI clients are in #2054, stacked on this PR.How it behaves
SecretStore(feat(dgw): add an encrypted in-memory secret store #2058): never in the job,gateway.dbor our logs. A restart loses them, so an unfinished Task fails withkeyLost.{ error, usage }.queuedcarriesusage: the tokens used so far over all attempts, plusunreportedRequestsfor requests the provider didn't count, a request still in flight included.gateway.db. Then the manifest lists it asai-analysis-0.slogand drops the earlier analysis. If that last step fails, a new job does it again later, after a restart too. A new Task that finds such a log first finishes it: it lists the log of a succeeded Task, and removes any other.languageis 1 to 10 characters, such asfr-CA; without it, English. It's a Task parameter, so the sametaskIdwith another language is atask_id_conflict.Where to look hardest
provisioner_tasks/runner/mod.rs:run_attempt(deadline watchdog, retry vs. fail,end_later) andresumeprovisioner_tasks/recording_ai_analysis/mod.rs:run,on_end,lock_sessionrecording.rs:handle_replace_artifactsKnown, accepted
All, reqwest/hyper's own TRACE lines show the provider's address (not the key). Our code never logs it.deadlinePassedinstead ofinternal, and a job queued for then removes its log.internal, before anything is paid.Tests
cargo test -p devolutions-gateway --lib: provisioner tasks, recording and secret store tests pass.cargo test -p devolutions-gateway-ai --lib recording_analysis: 45 passed.cargo test -p testsuite --test integration_tests ai_analysis: 10 passed, against the Gateway binary with a mock AI provider; the key and the base URL reach neither the logs norgateway.db.--features openapi --tests) and nightly rustfmt are clean.🤖 Generated with Claude Code