{"record":{"id":"fd45343567f3c0ee","repo":"Hmbown/CodeWhale","slug":"cloud-job-is-and-cannot-be-confirmed","errorCode":null,"errorMessage":"Cloud job {} is {} and cannot be confirmed.","messagePattern":"Cloud job (.+?) is (.+?) and cannot be confirmed\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/cloud_dispatch.rs","lineNumber":744,"sourceCode":"}\n\n/// Confirm a previously proposed job — in place, under the SAME id.\n///\n/// The record is mutated (`proposed` → `launching`, `confirmed = true`) and\n/// saved as itself; a second confirm finds `launching` and refuses. Routing\n/// through `execute_dispatch` instead would allocate a fresh job id (its ids\n/// hash the plan plus `unix_now()` at second granularity), leaving the\n/// proposal re-confirmable without limit — every confirm another sandbox\n/// and another PR.\npub fn confirm_job(\n    store: &CloudJobStore,\n    id: &str,\n    credentials: &CredentialState,\n    machine_token: &MachineTokenState,\n) -> Result<DispatchOutcome> {\n    let mut job = store.load(id)?;\n    if job.status != CloudJobStatus::Proposed {\n        bail!(\n            \"Cloud job {} is {} and cannot be confirmed.\",\n            job.id,\n            status_label(job.status)\n        );\n    }\n    job.confirmed = true;\n    if matches!(credentials, CredentialState::Missing) {\n        job.status = CloudJobStatus::Refused;\n        job.refusal = Some(missing_credentials_message());\n        job.note = missing_credentials_message();\n        job.finished_unix = Some(unix_now());\n        store.save(&job)?;\n        return Ok(DispatchOutcome::Refused(job));\n    }\n    if matches!(machine_token, MachineTokenState::Missing) {\n        job.status = CloudJobStatus::Refused;\n        job.refusal = Some(missing_machine_token_message());\n        job.note = missing_machine_token_message();","sourceCodeStart":726,"sourceCodeEnd":762,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/cloud_dispatch.rs#L726-L762","documentation":"confirm_job only transitions a cloud job out of the Proposed status. If the stored job is already in any later state (confirmed, dispatched, failed, cancelled, ...), the confirm call bails with the job's id and its human-readable status label. This is a state-machine guard preventing double confirmation and double dispatch.","triggerScenarios":"Calling confirm_job (directly or via dispatch) with an id whose persisted job has already been confirmed or has progressed past Proposed — e.g. confirming the same job twice, or re-running a dispatch after a partial failure that advanced the status.","commonSituations":"Double-clicking / retrying a confirm action in the UI; replaying a queued command after a crash; two sessions confirming the same proposed job concurrently; resuming an old job id from history.","solutions":["Reload the job (store.load) and check its status before calling confirm_job; only Proposed jobs are confirmable.","If the job is already confirmed/running, take the idempotent path: query its current outcome instead of confirming again.","If the job is in a terminal state (failed/cancelled), create a new proposed job rather than re-confirming.","Handle the race: if concurrent confirmation is possible, treat this error as 'someone else confirmed first' and re-read state."],"exampleFix":"// before\nconfirm_job(id, &creds, &token)?;\n// after\nlet job = store.load(id)?;\nif job.status == CloudJobStatus::Proposed {\n    confirm_job(id, &creds, &token)?;\n}","handlingStrategy":"try-catch","validationCode":"let job = store.load(id)?;\nlet confirmable = job.status == CloudJobStatus::Proposed;","typeGuard":"fn is_confirmable(job: &CloudJob) -> bool {\n    job.status == CloudJobStatus::Proposed\n}","tryCatchPattern":"match confirm_job(id, &creds, &token) {\n    Ok(outcome) => outcome,\n    Err(e) if e.to_string().contains(\"cannot be confirmed\") => {\n        let job = store.load(id)?;\n        eprintln!(\"job {} already {} — fetching current state\", job.id, status_label(job.status));\n        /* take idempotent read path */\n        /* ... */\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Check job.status == Proposed before confirming.","Design confirm actions as idempotent: re-read state instead of retrying blindly.","Disable the confirm control in the UI once a job leaves Proposed.","Guard against concurrent confirms (single-writer or optimistic check)."],"tags":["state-machine","cloud","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}