deadline_unix on an offer is one absolute clock. It decides whether a seller may
claim, and it also bounds how long the agent may run. A seller that claims late gets
only the remainder.
All citations are at aa172e660f5395c72db2124b0a37e7f6ba0ae05a.
offer posted ───── claim ───── award ───── deadline_unix
execution gets what is left ──┘
The claim gate
|
// Offer-freshness (money-safety): an offer whose own absolute deadline already passed is dead, |
|
// refused here before `job_deadline_unix` could hand it a fresh window. |
|
if offer.deadline_unix <= now_unix { |
|
return ClaimDecision::Skip(SkipReason::Lapsed); |
|
} |
// Offer-freshness (money-safety): an offer whose own absolute deadline already passed is dead,
// refused here before `job_deadline_unix` could hand it a fresh window.
if offer.deadline_unix <= now_unix {
return ClaimDecision::Skip(SkipReason::Lapsed);
}
The test is <=, so an offer with one second remaining is still claimable. Nothing
sets a floor on the window a claim may accept.
The same clock reaches execution
The stored row keeps the wire 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:
|
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 the agent runs under it:
|
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); |
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);
The timeout is the remainder, saturating to zero:
|
/// 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)) |
|
} |
pub fn unified_job_timeout(deadline_unix: u64, now_unix: u64) -> Duration {
Duration::from_secs(deadline_unix.saturating_sub(now_unix))
}
What that means for a buyer's contract
A 10-minute offer claimed 8 minutes in gives the runner about 2 minutes, not a fresh
10. Three details make the real budget smaller than "time since claim" suggests:
- Execution begins at award, not at claim, so the budget is
deadline − award_time.
MAX_AGENT_ATTEMPTS = 3 (run.rs:69) retries all live inside that same shrinking
window.
- A job awarded at or past the deadline receives
Duration::ZERO and fails at once.
The clocks after delivery are unaffected and are not part of this: the buyer's
payment window and the seller's result-publication retry window protect settlement
and retries. Neither extends agent execution.
The existing override does not supply the second clock
[seller].job_timeout_secs and --job-timeout-secs read as an execution-budget
override and are documented as one, but the value they produce never reaches
execution. That is filed separately as #944 — it is closeable on its own and does
not depend on this issue.
It is relevant here in one direction only: if the maintainers choose to make
job_timeout_secs live rather than remove it, the interaction between an operator's
budget and the buyer's deadline becomes the design question this issue is about.
Proposal, offered for discussion
Split the protocol into:
claim_deadline_unix — how long the offer stays claimable, and
execution_budget_secs — a budget that starts at award,
while keeping settlement as its own clock. A late claim then becomes safe and the
buyer's contract states one thing rather than two. Separately, a seller-side
soft-finalization buffer would address an agent that is nearly finished when the
window closes.
We have not implemented any of this and are not proposing an implementation here.
Open questions we can see, and do not answer:
- Who bounds
execution_budget_secs — the buyer on the offer, or the seller's
config, or a cap on both?
- Does the buyer's total exposure become
claim_deadline + execution_budget, and is
that acceptable?
- What happens to an already-deployed seller that reads offers with only the old
field?
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_exec.rs is byte-identical at all four (cmp). 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 |
| claim gate |
2134 |
2176 |
2176 |
offer_row writes the wire deadline |
1785 |
1785 |
1785 |
| execution reads the row |
5961 |
6003 |
6003 |
unified_job_timeout(deadline, …) |
5979 |
6028 |
6028 |
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.
What we did not check
- No build and no test run. Static reading at four revisions. We did not compile
a seller and observe a late claim receive a short window.
- No deployed binary was inspected. 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.
Labels
We cannot set labels on this repository. Requested: protocol, seller,
discussion — maintainer's call.
deadline_unixon an offer is one absolute clock. It decides whether a seller mayclaim, and it also bounds how long the agent may run. A seller that claims late gets
only the remainder.
All citations are at
aa172e660f5395c72db2124b0a37e7f6ba0ae05a.The claim gate
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 2174 to 2178 in aa172e6
The test is
<=, so an offer with one second remaining is still claimable. Nothingsets a floor on the window a claim may accept.
The same clock reaches execution
The stored row keeps the wire deadline:
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 1778 to 1786 in aa172e6
execute_jobreads that row: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 the agent runs under it:
maxplayerai/crates/maxplayer-core/src/seller_node/run.rs
Lines 6023 to 6028 in aa172e6
The timeout is the remainder, saturating to zero:
maxplayerai/crates/maxplayer-core/src/seller_exec.rs
Lines 1811 to 1817 in aa172e6
What that means for a buyer's contract
A 10-minute offer claimed 8 minutes in gives the runner about 2 minutes, not a fresh
10. Three details make the real budget smaller than "time since claim" suggests:
deadline − award_time.MAX_AGENT_ATTEMPTS = 3(run.rs:69) retries all live inside that same shrinkingwindow.
Duration::ZEROand fails at once.The clocks after delivery are unaffected and are not part of this: the buyer's
payment window and the seller's result-publication retry window protect settlement
and retries. Neither extends agent execution.
The existing override does not supply the second clock
[seller].job_timeout_secsand--job-timeout-secsread as an execution-budgetoverride and are documented as one, but the value they produce never reaches
execution. That is filed separately as #944 — it is closeable on its own and does
not depend on this issue.
It is relevant here in one direction only: if the maintainers choose to make
job_timeout_secslive rather than remove it, the interaction between an operator'sbudget and the buyer's deadline becomes the design question this issue is about.
Proposal, offered for discussion
Split the protocol into:
claim_deadline_unix— how long the offer stays claimable, andexecution_budget_secs— a budget that starts at award,while keeping settlement as its own clock. A late claim then becomes safe and the
buyer's contract states one thing rather than two. Separately, a seller-side
soft-finalization buffer would address an agent that is nearly finished when the
window closes.
We have not implemented any of this and are not proposing an implementation here.
Open questions we can see, and do not answer:
execution_budget_secs— the buyer on the offer, or the seller'sconfig, or a cap on both?
claim_deadline + execution_budget, and isthat acceptable?
field?
Versions
Read with
git show <rev>:<path>at four revisions:MakePrismsmain, as read54cf74b8334c64c5a1975bac698f68170c0e7301aa172e660f5395c72db2124b0a37e7f6ba0ae05av0.5.4b3dc79936923fbdc79870c7cb31266a02748add8v0.5.5ebaa796e6fea1b00c67769bf412ad4dc5f93d0e7seller_exec.rsis byte-identical at all four (cmp).run.rsdiffers, but eachsite is present at every rev with the same code; only the line numbers move:
aa172e66/54cf74b8offer_rowwrites the wire deadlineunified_job_timeout(deadline, …)aa172e66is 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.
What we did not check
a seller and observe a late claim receive a short window.
node.
seller_node/run.rs,seller_exec.rs,seller.rs,home.rs,sell.rs. The buyer side and the settlement clocks were not audited.Labels
We cannot set labels on this repository. Requested:
protocol,seller,discussion— maintainer's call.