Skip to content

Stop cloud sessions on cancel and read the v3 PR url - #15

Merged
TheOnlyFusionCube merged 2 commits into
TheOnlyFusionCube:mainfrom
hessabi:fix/cloud-cancel-and-pr-url-v2
Oct 25, 2026
Merged

TheOnlyFusionCube merged 2 commits into
TheOnlyFusionCube:mainfrom
hessabi:fix/cloud-cancel-and-pr-url-v2

Conversation

@hessabi

@hessabi hessabi commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Re-file of the former #3, as requested in #4, rebased onto current main. It now goes through the validated request path from #11.

What was wrong

  • /devin:cancel on a cloud handoff only sent a "please stop" chat message, so the remote session could keep running and spending ACUs.
  • getCloudSession read the PR url from pull_requests[0].url. v3 SessionResponse.pull_requests items are {pr_url, pr_state}, with no url, so service-key sessions never reported their PR.

What changed

  • New stopCloudSession sends DELETE to a documented terminate endpoint:
    • personal key: /v1/sessions/{session_id}
    • service key: /v3/organizations/{org_id}/sessions/{devin_id}
  • It goes through resolveApiBase and apiRequest, so the DEVIN_API_URL validation and the no-redirect rule apply.
  • cancel now calls it and reports whether the remote stop was confirmed.
    • API failures don't throw; the output says the stop couldn't be confirmed.
    • --json gains a remoteStopped field.
  • The PR url accessor checks, in order: pull_request.url (v1), pull_requests[0].pr_url (v3), then the old .url as a fallback.
  • README and AGENTS no longer say that cancel only asks the session to stop.

How tested

  • New tests/cloud.test.mjs, using a mocked globalThis.fetch with no network calls. It covers:
    • both DELETE URLs
    • a missing key
    • a rejected DEVIN_API_URL, which makes no request
    • HTTP and redirect errors
    • both PR url shapes
    • the cancel report wording
  • node --test: 76/76.

Cancelling a handoff job only posted a "please stop" chat message, so
the remote session kept running. Add stopCloudSession, which DELETEs
the session (v1: /v1/sessions/{session_id}; v3:
/v3/organizations/{org_id}/sessions/{devin_id}) through the same
validated apiRequest path, and use it in cancel. The report says
whether the remote stop was confirmed; API failures are tolerated.

getCloudSession read the PR url from pull_requests[0].url, but v3
returns {pr_url, pr_state}. Check pr_url first, keeping v1's
pull_request.url and the old accessor as fallbacks.

Adds tests/cloud.test.mjs with mocked fetch (no network).

@TheOnlyFusionCube TheOnlyFusionCube left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The v3 pr_url fallback and reuse of PR #11's validated, no-redirect request path look good. However, cloud cancellation needs a correctness fix before merge: cmdCancel marks the job permanently cancelled before attempting DELETE and swallows DELETE failures. If the API key is absent, validation rejects the endpoint, the network fails, or the remote session continues running, local status will never refresh it (refreshCloudJob only refreshes active jobs). This can conceal an actively billing cloud session. Please record an explicit cancel_pending / remote_stop_failed state (or otherwise retain a refreshable active status), preserve the API error/diagnostic, and confirm termination with a follow-up GET or documented definitive response before reporting remoteStopped: true. Add command-level tests for successful stop, API failure, missing key, and subsequent status/retry behavior; the current tests only exercise stopCloudSession and renderCancelReport independently. Also investigate the HOL Scanner delta: main/PR #14/#16 have one high, but this PR reports two highs (all 98/100). Do not weaken the scanner or treat the second high as baseline without identifying it.


// null: no remote stop attempted; false: attempted but not confirmed.
let remoteStopped = null;
if (job.kind === "handoff" && job.cloudSessionId && getCloudConfig().available) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocking: cancellation is recorded as terminal before remote termination. When this condition is false or DELETE rejects, the cloud session may keep running, but status refresh skips cancelled jobs. Keep failed/unconfirmed termination retryable and report a diagnostic; cover this path with an end-to-end command test.

cancel no longer marks a cloud handoff cancelled before the DELETE and
no longer swallows its failure. The job is set to cancel_pending first
(active, so status keeps refreshing it), and becomes cancelled only after
a follow-up GET shows the session exit, error, or suspended. The DELETE
response alone is never trusted. Failures keep a short diagnostic in
errorMessage (no key or API URL) and the report says how to retry.
status settles a cancel_pending job once the session is observed
stopped. --json keeps remoteStopped (true only when confirmed) and adds
the resulting status and remoteStatus.

Adds command-level tests against a local fake Devin API (success, API
failure and retry, missing key, status settling, service key v3 path) and
renames fake keys in tests/cloud.test.mjs to placeholders.
@hessabi

hessabi commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, agreed on both points. I've pushed a follow-up commit (no rebase).

cancel on a cloud handoff now writes cancel_pending first. That status is active, so status keeps refreshing the job. It becomes cancelled only after a follow-up GET shows the session as exit, error or suspended. The DELETE response alone is never trusted: v1 returns only {detail}, and v3's status isn't documented as the post-termination state. waiting_for_user and finished also don't count, since those sessions are still alive.

Failures keep a redacted diagnostic in errorMessage, and the report says the job is NOT cancelled and how to retry. A missing key makes no request. --json keeps remoteStopped (true only when confirmed) and adds status and remoteStatus.

Tests: command-level tests against a local fake Devin API cover success, API failure and retry, a missing key, status settling, and the v3 service-key path. 87 pass.

The second scanner high was a fake key at tests/cloud.test.mjs:7. It's now renamed to a placeholder the scanner skips, and the secrets check shows only the existing tests/cloud-url.test.mjs:19 finding, which a separate PR fixes. One related gap I left out of scope: the session-end hook still marks a --wait handoff cancelled without stopping the remote session. Happy to send that separately.

@TheOnlyFusionCube TheOnlyFusionCube left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Re-review: the prior blocker is addressed. cancel_pending is active/refreshable and retryable; remote termination is not declared successful from DELETE alone, but from GET showing exit/error/suspended. API errors retain sanitized diagnostics, and command-level tests cover v1/v3, missing key, failed DELETE/retry, status settlement, and local-only cancellation. The v3 pr_url fix retains PR #11's validated/no-redirect API path. Latest Test CI passes. The HOL scanner remains failing at repository level; its extra finding in the earlier PR revision was a fake test key, now replaced with a placeholder. Keep the scanner enabled and fix its remaining baseline finding separately. Follow-up recommended: address the separately disclosed session-end hook cancellation of --wait handoffs without stopping the remote session. Approved.

@TheOnlyFusionCube
TheOnlyFusionCube merged commit a9c4837 into TheOnlyFusionCube:main Oct 25, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants