syntax = "proto3";

package spindle.mill.v1;

import "buf/validate/validate.proto";

option go_package = "tangled.org/core/spindle/mill/proto/gen;millv1";

// Hello is the first frame an executor sends after dialing the mill. It
// carries the static traits of the node plus a resume hint.
message Hello {
  uint32 protocol_version = 1;
  // GOARCH of the node, retained as an informational trait for logs.
  string arch = 2;
  // opaque operator-defined labels used for runs_on matching.
  repeated string labels = 3;
}

// Resume is the mill's reply to Hello. The executor replays every buffered
// relay entry with offset strictly greater than ack_offset before sending new
// ones.
message Resume {
  uint64 ack_offset = 1;
}

// EngineAvailability is the changing per-engine state on a node. The executor
// reports opaque load metrics (higher = more loaded); the mill ranks by the
// worst load and uses the sum as a tie-breaker. The mill does not know what
// the metric keys mean.
message EngineAvailability {
  bool available = 1;
  // engine-defined load metrics. keys are opaque to the mill.
  map<string, double> load = 2;
  // engine-defined placement facts. keys are opaque to the mill and matched
  // exactly against requirements from the trusted engine planner.
  repeated string capabilities = 3;
}

// NodeSnapshot is pushed on connect, periodically, and right after any state
// change (reserve, commit, terminal).
message NodeSnapshot {
  string node_id = 1;
  uint64 seq = 2;
  map<string, EngineAvailability> engines = 3;
}

// ReserveSeat asks an executor to hold a seat for a job. Zero secrets ride this
// message; the raw pipeline/workflow are carried as JSON since processPipeline
// already round-trips them through JSON.
message ReserveSeat {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
  string target_engine = 2 [(buf.validate.field).string.min_len = 1];
  string raw_pipeline_json = 3;
  string raw_workflow_json = 4;
  // the pipeline id (knot + rkey); the executor reconstructs the exact
  // WorkflowId so its relayed status rows and log path match what the mill
  // authored for "pending".
  string knot = 5;
  string rkey = 6;
  uint32 ttl_seconds = 7;
}

enum RejectClass {
  REJECT_CLASS_UNSPECIFIED = 0;
  REJECT_CLASS_TRANSIENT = 1;
  REJECT_CLASS_INCOMPATIBLE = 2;
}

// ReserveResult is the executor's accept/reject for a ReserveSeat.
message ReserveResult {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
  bool accepted = 2;
  string reject_reason = 3;
  RejectClass reject_class = 4;
}

// Secret is a single unlocked secret, sent only inside CommitLease.
message Secret {
  string key = 1;
  string value = 2;
}

// CommitLease promotes a reservation to a running job and hands over the
// secrets. The executor then runs the real engine with the slot it already
// holds.
message CommitLease {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
  repeated Secret secrets = 2;
}

// Committed acks a CommitLease.
message Committed {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
}

// ReleaseLease tells the executor to drop a reservation it never committed (the
// mill picked another node, or is cleaning up).
message ReleaseLease {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
}

// CancelAttempt cancels a running attempt (user cancel / DestroyWorkflow).
message CancelAttempt {
  string lease_id = 1 [(buf.validate.field).string.min_len = 1];
  string reason = 2;
}

// StatusEvent relays a non-terminal status (i.e. running) the executor wrote to
// its own eventstream. Terminals are authored by the mill from the attempt
// result, so they are never relayed here. Carries a per-session monotonic
// offset for gap-free replay across reconnects.
message StatusEvent {
  uint64 offset = 1;
  string lease_id = 2 [(buf.validate.field).string.min_len = 1];
  string status = 3;
  string error = 4;
  int64 exit_code = 5;
  string workflow = 6;
  string pipeline_aturi = 7;
}

// LogLine relays one already-encoded models.LogLine JSON line. Carries an
// offset on the same per-session sequence as StatusEvent.
message LogLine {
  uint64 offset = 1;
  string lease_id = 2 [(buf.validate.field).string.min_len = 1];
  bytes raw_json = 3;
}

// AttemptResult is the terminal signal that wakes the mill's blocked RunStep
// and resolves the lease. terminal_status is one of success/failed/timeout/
// cancelled. Carries an offset on the same per-session sequence.
message AttemptResult {
  uint64 offset = 1;
  string lease_id = 2 [(buf.validate.field).string.min_len = 1];
  string terminal_status = 3;
  string error = 4;
  int64 exit_code = 5;
}

// Ack tells the executor the mill has durably processed all relay entries up
// to and including up_to_offset, so it may trim its buffer.
message Ack {
  uint64 up_to_offset = 1;
}

message Message {
  option (buf.validate.message).oneof = {
    fields: [
      "hello", "resume", "node_snapshot", "reserve_seat", "reserve_result",
      "commit_lease", "committed", "release_lease", "cancel_attempt",
      "status_event", "log_line", "attempt_result", "ack"
    ],
    required: true
  };

  Hello hello = 1;
  Resume resume = 2;
  NodeSnapshot node_snapshot = 3;
  ReserveSeat reserve_seat = 4;
  ReserveResult reserve_result = 5;
  CommitLease commit_lease = 6;
  Committed committed = 7;
  ReleaseLease release_lease = 8;
  CancelAttempt cancel_attempt = 9;
  StatusEvent status_event = 10;
  LogLine log_line = 11;
  AttemptResult attempt_result = 12;
  Ack ack = 13;
}
