unified_job_timeout documents a live precedence chain with --job-timeout-secs at
its head. That chain no longer decides anything. The value job_deadline_unix
computes reaches one operator log line and is then discarded; execution enforces the
offer's wire deadline instead.
All citations are at aa172e660f5395c72db2124b0a37e7f6ba0ae05a.
The documentation says the chain is live
|
/// The ONE coherent job timeout. The ACP driver's idle/response timeout is derived from the job's |
|
/// own deadline (`--job-timeout-secs` → offer deadline → default, via [`crate::seller::job_deadline_unix`]) |
|
/// so a job has a single predictable deadline. Saturating: a non-positive remaining window yields |
|
/// `Duration::ZERO`, which fails the run cleanly at the deadline rather than hanging. |
|
pub fn unified_job_timeout(deadline_unix: u64, now_unix: u64) -> Duration { |
|
Duration::from_secs(deadline_unix.saturating_sub(now_unix)) |
|
} |
/// The ONE coherent job timeout. The ACP driver's idle/response timeout is derived from the job's
/// own deadline (`--job-timeout-secs` → offer deadline → default, via [`crate::seller::job_deadline_unix`])
/// so a job has a single predictable deadline. Saturating: a non-positive remaining window yields
/// `Duration::ZERO`, which fails the run cleanly at the deadline rather than hanging.
pub fn unified_job_timeout(deadline_unix: u64, now_unix: u64) -> Duration {
Duration::from_secs(deadline_unix.saturating_sub(now_unix))
}
The config field documents the same override:
|
pub git_remote: String, |
|
/// Job deadline override (seconds). Default: offer `deadline_unix`, else ~600s. |
|
#[serde(default, skip_serializing_if = "Option::is_none")] |
|
pub job_timeout_secs: Option<u64>, |
/// Job deadline override (seconds). Default: offer `deadline_unix`, else ~600s.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_timeout_secs: Option<u64>,
The function honours it
|
/// Job deadline: config override, else offer deadline, else now+DEFAULT. |
|
pub fn job_deadline_unix(offer: &ParsedOffer, seller: &SellerConfig, now_unix: u64) -> u64 { |
|
if let Some(secs) = seller.job_timeout_secs { |
|
return now_unix.saturating_add(secs); |
|
} |
|
if offer.deadline_unix > now_unix { |
|
return offer.deadline_unix; |
|
} |
|
now_unix.saturating_add(DEFAULT_JOB_TIMEOUT_SECS) |
|
} |
/// Job deadline: config override, else offer deadline, else now+DEFAULT.
pub fn job_deadline_unix(offer: &ParsedOffer, seller: &SellerConfig, now_unix: u64) -> u64 {
if let Some(secs) = seller.job_timeout_secs {
return now_unix.saturating_add(secs);
}
if offer.deadline_unix > now_unix {
return offer.deadline_unix;
}
now_unix.saturating_add(DEFAULT_JOB_TIMEOUT_SECS)
}
It returns a fresh window measured from the moment of the call. That is exactly what
the config doc promises.
The result reaches one log line
job_deadline_unix has one call site in the repository:
|
ClaimDecision::Claim { |
|
deadline_unix: crate::seller::job_deadline_unix(offer, seller, now_unix), |
|
} |
ClaimDecision::Claim {
deadline_unix: crate::seller::job_deadline_unix(offer, seller, now_unix),
}
ClaimDecision::Claim has two non-test consumers. Both pass the value straight into
claim_offer:
|
ClaimDecision::Claim { deadline_unix } => { |
|
self.claim_offer( |
|
&row.offer_id, |
|
&row.buyer_pubkey, |
|
&offer, |
|
&seller_pubkey, |
|
deadline_unix, |
|
now, |
|
// A recorded offer re-driven from the store carries no tags; its pin (if any) |
|
// was already written at the original claim (INSERT OR IGNORE — idempotent). |
|
None, |
|
) |
|
let deadline_unix = match classify_offer(&offer, &seller, &self.agents, &seller_pubkey, &buyer_pubkey, now as u64, event.created_at.as_secs()) |
|
{ |
|
ClaimDecision::Claim { deadline_unix } => deadline_unix, |
|
let job_id = event.id.to_hex(); |
|
self.claim_offer( |
|
&job_id, |
|
&buyer_pubkey, |
|
&offer, |
|
&seller_pubkey, |
|
deadline_unix, |
|
now, |
|
contribution.as_ref(), |
|
) |
Inside claim_offer (run.rs:4880–5062) the parameter is read exactly once, in a
format string:
|
Ok(super::store::Claimed::New) => { |
|
opline!( |
|
"seller node claimed job_id={job_id} buyer={buyer_pubkey} amount={} deadline={deadline_unix} slot-reserved (awaiting award; {} slot(s) free)", |
|
offer.amount, |
|
self.slots.available() |
|
); |
Ok(super::store::Claimed::New) => {
opline!(
"seller node claimed job_id={job_id} buyer={buyer_pubkey} amount={} deadline={deadline_unix} slot-reserved (awaiting award; {} slot(s) free)",
A grep for deadline bounded to the function body returns three matches: the
parameter declaration at run.rs:4886, that log line, and an unrelated comment at
run.rs:5040. The value is not stored, not published on the claim event, and not
passed to claim_and_enqueue — that call uses its own CLAIM_PUBLISH_WINDOW_SECS
(run.rs:63).
What execution actually enforces
The stored row takes the wire offer's deadline:
|
fn offer_row(job_id: &str, buyer_pubkey: &str, offer: &ParsedOffer) -> super::store::Offer { |
|
super::store::Offer { |
|
offer_id: job_id.to_owned(), |
|
buyer_pubkey: buyer_pubkey.to_owned(), |
|
amount_sats: offer.amount, |
|
unit: offer.unit.clone(), |
|
task: offer.task.clone(), |
|
deadline_unix: offer.deadline_unix as i64, |
|
targeted: offer.is_targeted(), |
fn offer_row(job_id: &str, buyer_pubkey: &str, offer: &ParsedOffer) -> super::store::Offer {
super::store::Offer {
...
deadline_unix: offer.deadline_unix as i64,
execute_job reads that row and uses its deadline:
|
let offer = match self.node.store().offer_row(job_id) { |
|
Ok(offer) => offer, |
|
Err(error) => { |
|
opline!("seller node execute job_id={job_id}: offer read failed ({error}); treating deadline as live"); |
|
None |
|
} |
|
}; |
|
let deadline_unix = offer.as_ref().map(|offer| offer.deadline_unix); |
|
let deadline = offer.deadline_unix.max(0) as u64; |
let deadline = offer.deadline_unix.max(0) as u64;
…and hands it to the retry loop and to unified_job_timeout:
|
let run_result = run_agent_with_retry( |
|
deadline, |
|
MAX_AGENT_ATTEMPTS, |
|
|| now_unix() as u64, |
|
|_attempt| { |
|
let job_timeout = unified_job_timeout(deadline, now_unix() as u64); |
So an operator who sets job_timeout_secs sees the number in one claim log line and
nowhere else. The agent still runs under the offer's deadline.
The CLI flag is the same value, not an alternative
--job-timeout-secs writes the config key and stops there:
|
"--job-timeout-secs" => { |
|
index += 1; |
|
let raw = args |
|
.get(index) |
|
.ok_or_else(|| "missing value for --job-timeout-secs".to_owned())?; |
|
options.job_timeout_secs = Some( |
|
raw.parse() |
|
.map_err(|_| format!("--job-timeout-secs must be a u64, got {raw}"))?, |
|
); |
|
} |
|
let job_timeout_secs = options |
|
.job_timeout_secs |
|
.or_else(|| existing.as_ref().and_then(|seller| seller.job_timeout_secs)); |
let job_timeout_secs = options
.job_timeout_secs
.or_else(|| existing.as_ref().and_then(|seller| seller.job_timeout_secs));
…and that lands in SellerConfig at crates/maxplayer/src/sell.rs#L500. The flag and
the TOML key are one field, so there is no working flag beside a dead key.
Versions
Read with git show <rev>:<path> at four revisions:
| State |
Rev |
MakePrisms main, as read |
54cf74b8334c64c5a1975bac698f68170c0e7301 |
| citation rev for the links above |
aa172e660f5395c72db2124b0a37e7f6ba0ae05a |
v0.5.4 |
b3dc79936923fbdc79870c7cb31266a02748add8 |
v0.5.5 |
ebaa796e6fea1b00c67769bf412ad4dc5f93d0e7 |
seller.rs and seller_exec.rs are byte-identical at all four (cmp), so
job_deadline_unix and unified_job_timeout are the same code in every one.
run.rs differs, but each site is present at every rev with the same code; only the
line numbers move:
| Site |
v0.5.4 |
v0.5.5 |
aa172e66 / 54cf74b8 |
job_deadline_unix call |
2178 |
2220 |
2220 |
offer_row writes the wire deadline |
1785 |
1785 |
1785 |
| execution reads the row |
5961 |
6003 |
6003 |
unified_job_timeout(deadline, …) |
5979 |
6028 |
6028 |
log-only use in claim_offer |
4986 |
5028 |
5028 |
aa172e66 is in no release tag; it is an ancestor of main, so the link line numbers
are main line numbers. The clone this was read from last fetched MakePrisms at
2026-08-31T22:18:23Z; anything merged after that is outside what we read.
Two ways to close this
Either closes it alone, and the choice is a maintainer's:
- Wire the value through. Persist the
job_deadline_unix result on the job row
(or thread it to execute_job) so execution enforces the override the docs
describe. This gives job_timeout_secs the behaviour it advertises.
- Remove the override. Delete
job_timeout_secs and --job-timeout-secs, and
correct the two doc comments at home.rs:198 and seller_exec.rs:1812 so they
describe the offer deadline alone.
We are not proposing which. Option 1 has an interaction with the offer deadline that
belongs in #945; option 2 has none.
What we did not check
- No build and no test run. This is a static read at four revisions. We did not
compile a seller, set job_timeout_secs, and observe the timeout.
- No deployed binary was inspected. We read source at tags, not any operator's
running node.
- Files read:
seller_node/run.rs, seller_exec.rs, seller.rs, home.rs,
sell.rs. The buyer side and the settlement clocks were not audited.
Related
#945 asks whether the offer deadline should be split into a claim deadline and a
separate execution budget. It cites this issue as evidence. Neither issue depends
on the other: this one is closeable on its own, by either of the two ways above.
Labels
We cannot set labels on this repository. Requested: bug, seller —
maintainer's call.
unified_job_timeoutdocuments a live precedence chain with--job-timeout-secsatits head. That chain no longer decides anything. The value
job_deadline_unixcomputes reaches one operator log line and is then discarded; execution enforces the
offer's wire deadline instead.
All citations are at
aa172e660f5395c72db2124b0a37e7f6ba0ae05a.The documentation says the chain is live
maxplayerai/crates/maxplayer-core/src/seller_exec.rs
Lines 1811 to 1817 in aa172e6
The config field documents the same override:
maxplayerai/crates/maxplayer-core/src/home.rs
Lines 196 to 199 in aa172e6
The function honours it
maxplayerai/crates/maxplayer-core/src/seller.rs
Lines 110 to 119 in aa172e6
It returns a fresh window measured from the moment of the call. That is exactly what
the config doc promises.
The result reaches one log line
job_deadline_unixhas one call site in the repository:maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 2219 to 2221 in aa172e6
ClaimDecision::Claimhas two non-test consumers. Both pass the value straight intoclaim_offer:maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 3481 to 3492 in aa172e6
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 4798 to 4800 in aa172e6
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 4844 to 4853 in aa172e6
Inside
claim_offer(run.rs:4880–5062) the parameter is read exactly once, in aformat string:
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 5026 to 5031 in aa172e6
A grep for
deadlinebounded to the function body returns three matches: theparameter declaration at
run.rs:4886, that log line, and an unrelated comment atrun.rs:5040. The value is not stored, not published on the claim event, and notpassed to
claim_and_enqueue— that call uses its ownCLAIM_PUBLISH_WINDOW_SECS(
run.rs:63).What execution actually enforces
The stored row takes the wire offer's deadline:
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 1778 to 1786 in aa172e6
execute_jobreads that row and uses its deadline:maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 5803 to 5810 in aa172e6
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Line 6003 in aa172e6
…and hands it to the retry loop and to
unified_job_timeout:maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 6023 to 6028 in aa172e6
So an operator who sets
job_timeout_secssees the number in one claim log line andnowhere else. The agent still runs under the offer's deadline.
The CLI flag is the same value, not an alternative
--job-timeout-secswrites the config key and stops there:maxplayerai/crates/maxplayer/src/sell.rs
Lines 732 to 741 in aa172e6
maxplayerai/crates/maxplayer/src/sell.rs
Lines 344 to 346 in aa172e6
…and that lands in
SellerConfigatcrates/maxplayer/src/sell.rs#L500. The flag andthe TOML key are one field, so there is no working flag beside a dead key.
Versions
Read with
git show <rev>:<path>at four revisions:MakePrismsmain, as read54cf74b8334c64c5a1975bac698f68170c0e7301aa172e660f5395c72db2124b0a37e7f6ba0ae05av0.5.4b3dc79936923fbdc79870c7cb31266a02748add8v0.5.5ebaa796e6fea1b00c67769bf412ad4dc5f93d0e7seller.rsandseller_exec.rsare byte-identical at all four (cmp), sojob_deadline_unixandunified_job_timeoutare the same code in every one.run.rsdiffers, but each site is present at every rev with the same code; only theline numbers move:
aa172e66/54cf74b8job_deadline_unixcalloffer_rowwrites the wire deadlineunified_job_timeout(deadline, …)claim_offeraa172e66is in no release tag; it is an ancestor of main, so the link line numbersare main line numbers. The clone this was read from last fetched
MakePrismsat2026-08-31T22:18:23Z; anything merged after that is outside what we read.
Two ways to close this
Either closes it alone, and the choice is a maintainer's:
job_deadline_unixresult on the job row(or thread it to
execute_job) so execution enforces the override the docsdescribe. This gives
job_timeout_secsthe behaviour it advertises.job_timeout_secsand--job-timeout-secs, andcorrect the two doc comments at
home.rs:198andseller_exec.rs:1812so theydescribe the offer deadline alone.
We are not proposing which. Option 1 has an interaction with the offer deadline that
belongs in #945; option 2 has none.
What we did not check
compile a seller, set
job_timeout_secs, and observe the timeout.running node.
seller_node/run.rs,seller_exec.rs,seller.rs,home.rs,sell.rs. The buyer side and the settlement clocks were not audited.Related
#945 asks whether the offer deadline should be split into a claim deadline and a
separate execution budget. It cites this issue as evidence. Neither issue depends
on the other: this one is closeable on its own, by either of the two ways above.
Labels
We cannot set labels on this repository. Requested:
bug,seller—maintainer's call.