use std::fs;
use serde_json::json;
use crate::domain::ticket::TicketSnapshot;
use crate::flow::{Flow, VerdictPolicy};
use crate::protocol::{ErrorBody, Request, RequestId, ResponseEnvelope, VerdictArgs, VerdictValue};
use crate::vendor_error::VendorErrorMatch;
use super::commands::lookup;
use super::dispatcher::{DispatcherState, conflict, internal, mark_storage_full, unauthorized};
pub(super) fn dispatch_worker(
state: &mut DispatcherState,
id: RequestId,
request: Request,
run_id: &str,
token: Option<&str>,
) -> ResponseEnvelope {
let valid = token.is_some_and(|presented| {
state
.worker_tokens
.get(run_id)
.is_some_and(|issued| issued == presented)
});
if !valid {
return ResponseEnvelope::failure(
Some(id),
unauthorized("the presented token is not valid for this run"),
);
}
let data = match request {
Request::Brief(_) => handle_brief(state, run_id),
Request::Show(args) => match args.reference.as_deref() {
Some(reference) if args.limit.is_none() => handle_show(state, run_id, reference),
_ => Err(unauthorized(
"workers may only show their own run's ticket by exact id",
)),
},
Request::Note(args) => handle_note(state, run_id, &args.text),
Request::Verdict(args) => handle_verdict(state, run_id, &args),
_ => Err(unauthorized(
"operator verbs are not available on a worker socket",
)),
};
match data {
Ok(data) => ResponseEnvelope::success(Some(id), data),
Err(error) => ResponseEnvelope::failure(Some(id), error),
}
}
fn handle_brief(state: &DispatcherState, run_id: &str) -> Result<serde_json::Value, ErrorBody> {
let run = lookup(state, |store| store.run(run_id))?
.ok_or_else(|| internal("the run for this token no longer exists"))?;
let ticket = match run.ticket_json.as_deref() {
Some(snapshot) => serde_json::from_str::<TicketSnapshot>(snapshot)
.map_err(|error| internal(&format!("the run's ticket snapshot is invalid: {error}")))?,
None => {
let ticket = lookup(state, |store| store.ticket(&run.ticket_id))?
.ok_or_else(|| internal("the ticket for this run no longer exists"))?;
let body = ticket.body.unwrap_or_else(|| {
ticket
.file_path
.as_ref()
.and_then(|file_path| fs::read_to_string(state.root.join(file_path)).ok())
.unwrap_or_default()
});
TicketSnapshot {
id: ticket.id,
name: ticket.name,
blocked_by: ticket.blocked_by,
worktree: ticket.worktree,
target: ticket.target,
model: ticket.model,
effort: ticket.effort,
body,
}
}
};
let mut definition_of_done = vec!["Commit your work to the run branch".to_owned()];
if state.aftercare_test_cmd.is_some() {
definition_of_done.push("The configured test command passes".to_owned());
}
Ok(json!({
"run": run_id,
"ticket": {
"id": ticket.id,
"name": ticket.name,
"blocked_by": ticket.blocked_by,
"worktree": ticket.worktree,
"body": ticket.body,
"acceptance": [],
"target": ticket.target,
"model": ticket.model,
"effort": ticket.effort,
},
"worktree": run.worktree_path,
"branch": run.branch,
"definition_of_done": definition_of_done,
}))
}
fn handle_show(
state: &DispatcherState,
run_id: &str,
reference: &str,
) -> Result<serde_json::Value, ErrorBody> {
let run = lookup(state, |store| store.run(run_id))?
.ok_or_else(|| internal("the run for this token no longer exists"))?;
if reference != run.ticket_id {
return Err(unauthorized("workers may only show their own run's ticket"));
}
let ticket = lookup(state, |store| store.ticket(&run.ticket_id))?
.ok_or_else(|| internal("the ticket for this run no longer exists"))?;
let vendor_error = current_ticket_vendor_error(state, &ticket)?;
Ok(ticket_show(reference, &ticket, vendor_error.as_ref()))
}
pub(super) fn ticket_show(
reference: &str,
ticket: &crate::store::TicketRecord,
vendor_error: Option<&VendorErrorMatch>,
) -> serde_json::Value {
json!({
"ref": reference,
"kind": "ticket",
"value": {
"id": ticket.id,
"project": ticket.project_id,
"state": ticket.state,
"file": ticket.file_path,
"name": ticket.name,
"blocked_by": ticket.blocked_by,
"worktree": ticket.worktree,
"target": ticket.target,
"model": ticket.model,
"effort": ticket.effort,
"reason": vendor_error.map(|error| error.diagnostic.as_str()),
"classification": vendor_error,
},
})
}
pub(super) fn current_ticket_vendor_error(
state: &DispatcherState,
ticket: &crate::store::TicketRecord,
) -> Result<Option<VendorErrorMatch>, ErrorBody> {
let vendor_error = lookup(state, |store| {
store.latest_vendor_error_for_ticket(&ticket.id)
})?;
if ticket.state != "ready" {
return Ok(vendor_error);
}
let cooldown_active = match ticket.target.as_deref() {
Some(target) => lookup(state, |store| {
store.active_cooldown_for_target(target, state.clock.now_ms())
})?
.is_some(),
None => false,
};
Ok(vendor_error.filter(|error| error.class.requires_cooldown() && cooldown_active))
}
fn handle_note(
state: &DispatcherState,
run_id: &str,
text: &str,
) -> Result<serde_json::Value, ErrorBody> {
let ordinal = lookup(state, |store| store.next_note_ordinal())?;
let note_id = format!("N{ordinal}");
state
.store
.insert_note(¬e_id, run_id, text, state.clock.now_ms())
.map_err(|error| {
mark_storage_full(state, &error);
internal(&format!("cannot record note: {error}"))
})?;
Ok(json!({"note": {"id": note_id, "run": run_id, "text": text}}))
}
fn handle_verdict(
state: &DispatcherState,
run_id: &str,
args: &VerdictArgs,
) -> Result<serde_json::Value, ErrorBody> {
let run = lookup(state, |store| store.run(run_id))?
.ok_or_else(|| internal("the run for this token no longer exists"))?;
let snapshot = run
.flow_json
.as_deref()
.ok_or_else(|| internal("the run has no flow snapshot"))?;
let flow: Flow = serde_json::from_str(snapshot)
.map_err(|error| internal(&format!("the run's flow snapshot is invalid: {error}")))?;
let stage_name = match run.state.as_str() {
"running" => flow
.stages
.first()
.map(|stage| stage.name.clone())
.ok_or_else(|| internal("the run's flow has no first stage"))?,
"aftercare" => {
let rows = lookup(state, |store| store.run_evidence(run_id))?;
rows.iter()
.find(|(kind, _)| kind == "aftercare_process")
.and_then(|(_, data)| serde_json::from_str::<serde_json::Value>(data).ok())
.and_then(|data| data["stage"].as_str().map(str::to_owned))
.ok_or_else(|| conflict("the run has no stage process currently executing"))?
}
_ => return Err(conflict("the run has no stage currently executing")),
};
let stage = flow
.stages
.iter()
.find(|stage| stage.name == stage_name)
.ok_or_else(|| internal("the executing stage is not in the run's flow snapshot"))?;
if stage.verdict != VerdictPolicy::Reported {
return Err(unauthorized(&format!(
"stage `{stage_name}` does not use the reported verdict policy"
)));
}
let verdict = match args.verdict {
VerdictValue::Pass => "pass",
VerdictValue::Fail => "fail",
};
let inserted = state
.store
.record_stage_verdict(
run_id,
&stage_name,
verdict,
args.reason.as_deref(),
state.clock.now_ms(),
)
.map_err(|error| {
mark_storage_full(state, &error);
internal(&format!("cannot record stage verdict: {error}"))
})?;
if !inserted {
return Err(conflict(&format!(
"stage `{stage_name}` already has a reported verdict"
)));
}
Ok(json!({
"verdict": {
"run": run_id,
"stage": stage_name,
"verdict": verdict,
"reason": args.reason,
}
}))
}