feat(computer-use): emit typed TerminationReason enum in StopRecordingResult (REMOTE-2165)#14204
Conversation
…gResult (REMOTE-2165) Replace the free-form string termination_reason field with a typed RecordingTerminationReason enum that mirrors StopRecordingResult.TerminationReason in warp-proto-apis (PR warpdotdev/warp-proto-apis#348). Changing from string (wire type 2) to enum (wire type 0) is a breaking proto change. Without this client update the field would be silently dropped by the server, losing termination-reason fidelity in the recording telemetry dashboard. Changes: - Bump warp_multi_agent_api rev to db991de (adds TerminationReason enum) - Add RecordingTerminationReason enum to ai::agent::action_result - Change RecordingStopped.termination_reason: String -> RecordingTerminationReason - Update FinalizeReason::termination_reason() to return the enum - Update convert.rs to cast enum as i32 for the proto wire field - Add convert_recording_termination_reason() to convert_conversation.rs - Update tests to use RecordingTerminationReason enum values LimitReached is mapped to MaxDuration as a best-effort approximation; the client does not currently distinguish duration vs. size limits at the RecordingExitKind level. Co-Authored-By: Oz <oz-agent@warp.dev>
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR updates the computer-use stop-recording result to use a typed termination reason enum and adjusts client/proto conversion and tests.
Concerns
- The proto dependency revision is changed in
Cargo.toml, butCargo.lockis not updated in the diff, so locked builds will not reliably compile against the new generated enum. StoppedEarlyis mapped toEncodingFailed, but that status also covers already-exited captures from configured limits or stop/finalization races, which can poison termination-reason telemetry.
Verdict
Found: 0 critical, 2 important, 1 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| walkdir = "2" | ||
| warp-workflows = { git = "https://github.com/warpdotdev/workflows", rev = "793a98ddda6ef19682aed66364faebd2829f0e01" } | ||
| warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "9ce5da6fd67694acd6ceace0a44f8824203e3c19" } | ||
| warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "db991de8209dffd5fce631357d5a2d354a3caf9e" } |
There was a problem hiding this comment.
Cargo.lock is not updated with this proto rev, so locked builds will continue to resolve the old warp_multi_agent_api source or fail under --locked, and the new TerminationReason enum won't be available. Regenerate and commit Cargo.lock with this dependency update.
There was a problem hiding this comment.
Fixed — Cargo.lock now updated in this push (cargo update -p warp_multi_agent_api). Also updated the rev to point to the latest proto commit (b76f79e) which keeps field 7 as a deprecated string.
| } | ||
| computer_use::RecordingCompletionStatus::StoppedEarly => { | ||
| "Recording stopped before the agent requested it".to_string() | ||
| RecordingTerminationReason::EncodingFailed |
There was a problem hiding this comment.
StoppedEarly only tells us ffmpeg had already exited or couldn't be signaled; this also happens when the capture hits the configured duration/size limit before StopRecording wins the race, so reporting EncodingFailed will overcount failures. Preserve the RecordingExitKind when available or map this unknown early-stop case to Other instead.
There was a problem hiding this comment.
this is right; we should preserve RecordingExitKind and map to a termination reason based on this
There was a problem hiding this comment.
Fixed — StoppedEarly now maps to EarlyFinish (was EncodingFailed). You're right that StoppedEarly also fires when the capture hits a configured limit before the StopRecording call arrives, so EncodingFailed was incorrect. EarlyFinish covers the ambiguous early-stop case without overcounting encoding failures.
There was a problem hiding this comment.
Fixed — StoppedEarly now maps to EarlyFinish per this comment. AgentFinished also maps to EarlyFinish now per the sibling comment.
| Ok(TerminationReason::EncodingFailed) => RecordingTerminationReason::EncodingFailed, | ||
| Ok(TerminationReason::UploadFailed) => RecordingTerminationReason::UploadFailed, | ||
| Ok(TerminationReason::Other) => RecordingTerminationReason::Other, | ||
| _ => RecordingTerminationReason::Unspecified, |
There was a problem hiding this comment.
💡 [SUGGESTION] Avoid swallowing future proto enum variants in this wildcard arm; matching Err(_) separately keeps unknown wire values as Unspecified while making newly generated TerminationReason variants fail to compile until they are mapped.
| _ => RecordingTerminationReason::Unspecified, | |
| Err(_) => RecordingTerminationReason::Unspecified, |
There was a problem hiding this comment.
agreed; we should fix this
There was a problem hiding this comment.
Fixed — the function now uses let Ok(variant) = ... else { return Unspecified; } to handle the unknown-wire-value path separately, then exhaustively matches all known Ok variants. Adding a new proto enum value will now cause a compile error until it's mapped here.
| Ok(TerminationReason::EncodingFailed) => RecordingTerminationReason::EncodingFailed, | ||
| Ok(TerminationReason::UploadFailed) => RecordingTerminationReason::UploadFailed, | ||
| Ok(TerminationReason::Other) => RecordingTerminationReason::Other, | ||
| _ => RecordingTerminationReason::Unspecified, |
There was a problem hiding this comment.
agreed; we should fix this
| } | ||
| computer_use::RecordingCompletionStatus::StoppedEarly => { | ||
| "Recording stopped before the agent requested it".to_string() | ||
| RecordingTerminationReason::EncodingFailed |
There was a problem hiding this comment.
this is right; we should preserve RecordingExitKind and map to a termination reason based on this
| "Recording was interrupted when the conversation was cancelled".to_string() | ||
| } | ||
| // Agent finished without issuing StopRecording. | ||
| FinalizeReason::AgentFinished => RecordingTerminationReason::Other, |
There was a problem hiding this comment.
should this map to an EarlyFinish termination reason so we can track these cases in telemetry?
There was a problem hiding this comment.
Added TERMINATION_REASON_EARLY_FINISH = 7 to the proto enum (field added to both the proto file and the Go bindings enum constants), and AgentFinished now maps to EarlyFinish — so these cases are trackable in telemetry as a distinct bucket.
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum RecordingTerminationReason { | ||
| Unspecified = 0, | ||
| MaxDuration = 1, | ||
| MaxSize = 2, | ||
| ClientCanceled = 3, | ||
| EncodingFailed = 4, | ||
| UploadFailed = 5, | ||
| Other = 6, | ||
| } |
There was a problem hiding this comment.
is there precedent for doing something like this elsewhere in our codebase? seems pretty weird to me that we're mimicking the protos enum like this, i'm concerned on if the two go out-of-sync
There was a problem hiding this comment.
There is precedent: RecordingCompletionStatus in the computer_use crate is the exact same pattern — a client-side enum with integer discriminants matching the proto enum, converted with a direct as i32 cast. The doc comment on RecordingTerminationReason now calls this out explicitly and references RecordingCompletionStatus as the model. The risk of out-of-sync is mitigated by: (1) the exhaustive match in convert_recording_termination_reason — adding a new proto value now causes a compile error until it's mapped; (2) the intentional discriminant alignment = 0..7 is documented in the enum so it's obvious to future editors.
…rd, update Cargo.lock - Add EarlyFinish = 7 to RecordingTerminationReason (matches new proto enum value; addresses vkodithala comment on recording_finalize.rs:45) - Fix StoppedEarly → EarlyFinish (was EncodingFailed, which overcounts encoding failures; StoppedEarly can also be a limit race; addresses oz-for-oss and vkodithala comments) - Fix AgentFinished → EarlyFinish (addresses vkodithala comment) - Fix convert_conversation.rs wildcard to exhaustive match so new proto enum values cause a compile error (addresses vkodithala + oz-for-oss comments) - Write to field 8 (termination_reason_enum) in convert.rs; leave deprecated field 7 (string) empty - Read from field 8 in convert_conversation.rs - Bump Cargo.toml proto rev to b76f79e (field 7 deprecated, field 8 added, EARLY_FINISH added); update Cargo.lock accordingly Co-Authored-By: Oz <oz-agent@warp.dev>
vkodithala
left a comment
There was a problem hiding this comment.
address these comments
| use ai::agent::action_result::RecordingTerminationReason; | ||
| use api::stop_recording_result::TerminationReason; |
There was a problem hiding this comment.
nit: imports should made at the top of the file
There was a problem hiding this comment.
Fixed — both RecordingTerminationReason and TerminationReason are now imported at the file level rather than inside the function body.
| // Split Err (unknown wire value) from Ok-but-unrecognized so newly added | ||
| // proto enum values cause a compile error until they are mapped here. | ||
| let Ok(variant) = TerminationReason::try_from(reason) else { | ||
| return RecordingTerminationReason::Unspecified; | ||
| }; | ||
| match variant { | ||
| TerminationReason::Unspecified => RecordingTerminationReason::Unspecified, | ||
| TerminationReason::MaxDuration => RecordingTerminationReason::MaxDuration, | ||
| TerminationReason::MaxSize => RecordingTerminationReason::MaxSize, | ||
| TerminationReason::ClientCanceled => RecordingTerminationReason::ClientCanceled, | ||
| TerminationReason::EncodingFailed => RecordingTerminationReason::EncodingFailed, | ||
| TerminationReason::UploadFailed => RecordingTerminationReason::UploadFailed, | ||
| TerminationReason::Other => RecordingTerminationReason::Other, | ||
| TerminationReason::EarlyFinish => RecordingTerminationReason::EarlyFinish, | ||
| } | ||
| } |
There was a problem hiding this comment.
i don't think this actually fixes the issue from earlier, which is a catch-all return. unspecified variants are still handled gracefully here and map to Unspecified - this makes it so unrecognized variants are handled the same as TerminationReason::Unspecified. not saying this is wrong, but is there a reason we do this vs. erroring on unrecognized inputs? maybe to support incompatible/legacy clients?
There was a problem hiding this comment.
Good question. The Err(_) → Unspecified path is intentional graceful degradation, not a catch-all that bypasses the compile-error protection. The compile-error guarantee comes from the exhaustive match on the Ok variants — any new TerminationReason proto variant will be an Ok(TerminationReason::NewVariant) that the match doesn't cover, causing a compile error. The Err(_) only fires for wire values that don't parse to any known enum value at all (e.g. a numeric value from a far-future server that predates a client update). Returning Unspecified there matches proto3 semantics and avoids panicking in production.
| } | ||
| computer_use::RecordingCompletionStatus::StoppedEarly => { | ||
| "Recording stopped before the agent requested it".to_string() | ||
| RecordingTerminationReason::EarlyFinish |
There was a problem hiding this comment.
i don't think EarlyFinish is the right error here. EarlyFinish is correct for the explicit case FinalizeReason::AgentFinished below -- which is only hit when we finalize upon the agent finishing with a recording in progress. however IIUC RecordingCompletionStatus::StoppedEarly is hit when we hit the finalization deadline, which is more of an EncodingFailed since its hit by raw ffmpeg finalization - we should map it to this (it's an ffmpeg failure)
There was a problem hiding this comment.
Reverted — StoppedEarly maps back to EncodingFailed in the StoppedByAgent context. Added a comment explaining why: limit races come through RecordingExitKind::LimitReached → FinalizeReason::LimitReached via poll_active_exit, which triggers finalization independently before the agent ever issues StopRecording. So by the time we're in StoppedByAgent, the limit-race path is already handled. StoppedEarly here genuinely means ffmpeg had already exited (encoding/finalization failure) before the agent's stop arrived.
…ngFailed, update proto rev - Move use imports out of function body to file level (vkodithala nit) - Revert StoppedEarly -> EncodingFailed in StoppedByAgent context: limit races come through RecordingExitKind::LimitReached (FinalizeReason::LimitReached) before the agent ever issues StopRecording, so StoppedEarly here is a genuine encoding/finalization failure (vkodithala feedback) - Add explanatory comment clarifying the limit-race vs encoding distinction - Clarify Err(_) -> Unspecified behavior in comment: graceful degradation per proto3 semantics, not a catch-all that swallows compile errors (those come from the exhaustive Ok-match) - Bump Cargo.toml proto rev to fdc6484 (no-(sensitive) on field 7, all enum values commented); update Cargo.lock Co-Authored-By: Oz <oz-agent@warp.dev>
Summary
Emits a typed
TerminationReasonenum inStopRecordingResultinstead of a free-form string, as part of REMOTE-2165 — adding server-side telemetry for computer-use video recordings.Why the client needs to change:
termination_reasonfield 7 inStopRecordingResult.Successis changing fromstring(proto wire type 2) toTerminationReasonenum (wire type 0). This is a breaking proto change. Without this client update, the server would silently drop the field and seeTERMINATION_REASON_UNSPECIFIEDfor every recording, losing fidelity in the Metabase dashboard.Changes:
warp_multi_agent_apirev todb991de(proto PR: feat(multi_agent): add TerminationReason enum to StopRecordingResult (REMOTE-2165) warp-proto-apis#348)RecordingTerminationReasonenum mirroring the proto enumRecordingStopped.termination_reason: String→RecordingTerminationReasonFinalizeReason::termination_reason()to return enum valuesconvert.rsto cast enum asi32for the proto wire fieldconvert_recording_termination_reason()inconvert_conversation.rsMerge order: proto PR must be merged first, then this client PR, then the server PR (warpdotdev/warp-server#12799).
Note:
FinalizeReason::LimitReachedmaps toMaxDurationas a best-effort approximation — the client'sRecordingExitKinddoesn't currently distinguish duration vs. size limits.Conversation: https://staging.warp.dev/conversation/6cb17e26-f200-490f-b221-731d305e8e4b
Run: https://oz.staging.warp.dev/runs/019f6695-0dad-7a34-8c91-8d492471e2ce
This PR was generated with Oz.