Skip to content

feat(dgw): list Gateway-generated artifacts in the recording manifest - #2003

Open
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 20 commits into
masterfrom
feat/jrec-manifest-logs
Open

irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 20 commits into
masterfrom
feat/jrec-manifest-logs

Conversation

@irvingoujAtDevolution

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Lets Gateway attach its own non-recording artifacts (AI analysis first) to a session recording, without breaking released players, which treat every files entry as playable.

  • the manifest gets an artifacts object next to files, hard-typed per kind (only ai-analysis today):
    {
      "files": [ { "fileName": "recording-0.webm", "startTime": 1787255035, "duration": 25 } ],
      "artifacts": { "ai-analysis": [ { "fileName": "ai-analysis-0.slog" } ] }
    }
  • artifacts are written by Gateway only, through RecordingMessageSender::add_artifact; there is no artifact push, so a push token can't upload one. Nothing calls it yet: the first caller is the AI log task in feat(dgw): generate a session log with AI #2008
  • JREC push is unchanged, slog included: AD console .slog is that session's recording and stays in files
  • artifacts stay out of the recording lifecycle: no recording policy, disconnect TTL, /shadow or duration handling
  • an artifact can be added before any recording or while one is being pushed; the recording re-reads the manifest on disconnect, so neither entry is lost
  • artifacts is omitted when empty, so existing manifests are byte-for-byte the same
  • session ZIP downloads include artifacts

Design notes: devolutions-gateway/src/recording.intent.md.

Tested: artifacts:: 1/1, recording:: 8/8, api::jrec:: 13/13, dvls_compatibility 24/24.

🤖 Generated with Claude Code

A .slog pushed for a session that already has a recording is now listed
in a new `logs` field of recording.json, named `log-{n}.slog`.
The `files` list keeps holding recordings only, so released players and
streamers keep working.
A log-only session (AD Console) still lists its .slog in `files`.
The `logs` field is omitted when empty, so existing manifests stay the same.
The session ZIP download now includes the logs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

A post-session .slog push kept the session "connected", so /shadow
streamed the finished recording instead of refusing like master did.
/shadow now closes with "streaming ended" when the ongoing push is a
log, and log chunks no longer wake /shadow streamers.

The ongoing push now tracks which manifest entry it writes to
(recording or log, by index) instead of a bool, which removes the
"no log file" bug branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ZIP now holds logs as well as recordings, so "clip" no longer fits.
The public endpoint doc is unchanged, so the OpenAPI output is too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Routing .slog pushes into `logs` by file type changed where existing
clients' .slog streams land, which breaks backward compatibility.

JREC push now takes an optional `materialType` query parameter:
`recording` (the default when absent) or `log`. Recording material
requires `fileType` and is stored in `files` as before, including
`slog`. Log material rejects `fileType`, is opaque to Gateway, and is
stored in `logs` as `log-N.slog`, even before any recording exists.
Invalid combinations are refused with HTTP 400 before the upgrade.
@irvingoujAtDevolution
irvingouj@Devolutions (irvingoujAtDevolution) marked this pull request as ready for review September 28, 2026 19:52
Copilot AI balanced review requested due to automatic review settings September 28, 2026 19:52
irvingouj@Devolutions (irvingoujAtDevolution) added a commit that referenced this pull request Sep 28, 2026
Runs the ai-log task end to end: streams the terminal recording into chunk
files in a task workspace, describes each chunk with the AI (checkpointed so a
retry resumes, splitting a chunk when the answer is cut at the output limit),
writes the .slog and appends it to the manifest `logs` through the recording
manager. Also reports truncated answers from the AI crate and regenerates the
ai-log substate docs.

Re-stacked on #2003 (materialType): squashes the earlier bcebf25, 707ab33,
9b6404b, 40d675b and a5ae0ad. Adding a log no longer requires a recording
in `files`, per recording.intent.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Logs are pushed to `/jet/jrec/push/{id}/logs` instead of
`?materialType=log`, so a log push has no `fileType` to validate.
`/jet/jrec/push/{id}?fileType=...` is unchanged, including `slog`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread devolutions-gateway/src/recording.intent.md
Follows Benoit's route 2: one push route, `?fileType=slog&category=log`
stores the stream in `logs`, and a missing `category` still means
recording. Keeping `fileType` for logs leaves room for other log formats;
only `slog` is accepted today.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…file type

`PushMaterial` becomes `PushCategory`, matching the `category` query
parameter. A log push now carries its file type and is named after its
extension (`log-N.<ext>`), so only the API decides which formats may be
pushed as logs; today that is `slog`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces the `logs` list with an `artifacts` object keyed by role, as
discussed with Benoit: `?fileType=slog&artifact=ai-analysis` stores the
stream as `ai-analysis-N.slog` under `artifacts["ai-analysis"]`. A push
without `artifact` is a recording and lands in `files` as before, `slog`
included. Roles are a fixed enum for pushes, but the manifest keeps its
keys as strings so roles written by a newer Gateway survive a rewrite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) changed the title feat(dgw): list session recording logs separately in the manifest feat(dgw): list non-recording artifacts by role in the recording manifest Sep 29, 2026
`PushCategory` is a leftover of the `category` parameter; the query now
uses `artifact`, so the destination is a `PushTarget`. A test checks that
each role's manifest key matches its query value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f the recording policy

The push query takes `kind=<role>` instead of `artifact=<role>`, as
written in recording.intent.md.

An artifact push no longer satisfies a session's recording policy:
`EnsureRecordingPolicyTask` now asks `is_recording`, which only counts
Recording pushes, so pushing an ai-analysis log alone can't keep a
session that must be recorded alive. An artifact push also never carries
the policy, so its end doesn't kill the session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ording policy

Ongoing pushes are now keyed by (session, kind). Before, an artifact push
replaced the session's single entry, which dropped a disconnected
must-be-recorded Recording's TTL kill and blocked its reconnect.

- Only the Recording kind drives the recording policy, active_recordings,
  /shadow wake-ups and manifest timing; artifact kinds touch none of them.
- A Recording and an artifact may push at the same time; one push per kind.
- The manager reads and writes the manifest from disk for each change, so
  concurrent pushes keep each other's entries.
- PushTarget::new owns the "ai-analysis needs slog" rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`artifacts` is a struct with one list per `ArtifactKind` instead of a
string-keyed map, reached through an exhaustive match, so adding a kind
forces every reader and writer to handle it. Kinds unknown to this
Gateway are no longer kept when it rewrites a manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ArtifactKind`, the manifest `artifacts` struct and its entries, and the
file-type error now live in their own module. The session ZIP reuses the
same typed struct instead of a second copy of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Artifact pushes no longer take any part in the recording lifecycle: the
recording manager only allocates the next `<kind>-N.<ext>` name and adds
it to the manifest `artifacts` list, then the push streams bytes to the
file. No ongoing entry, disconnect, TTL, recording policy, active
recordings, shadow wake-up or timing writes.

Recording pushes are back to master's shape (one per session, keyed by
session ID). Their disconnect re-reads the manifest from disk and
updates its own `files` entry, so artifact entries added meanwhile are
kept.

Removes `KindPolicy`, `PushKind`, `is_recording` and the test-only fake
session manager.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Describes recording pushes, artifact pushes with `kind`, and that
artifacts take no part in the recording policy or session shadowing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A session has one recording push at a time and artifacts never go in
`files`, so the pushing recording is always the last entry. Drop the
stored index; only re-read the manifest from disk before updating it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep master's `clip_names` naming and docs for the session ZIP, build the
`PushTarget` directly in the push handler instead of through a wrapper,
and leave the "ai-analysis must be slog" check to the recording tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The JREC push endpoint and ClientPush go back to master. Artifacts are
added through RecordingMessageSender::add_artifact, which moves a file
into the session folder and lists it in the manifest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) changed the title feat(dgw): list non-recording artifacts by role in the recording manifest feat(dgw): list Gateway-generated artifacts in the recording manifest Sep 29, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants