Category: bug Severity: major
Location: lib/arcp/job/job_error.rb:26-28 (secondary: lib/arcp/client.rb:214, lib/arcp/client.rb:300)
Spec: ARCP v1.1 §12
What
Spec §12: "Error payloads carry a retryable boolean." The JobError value object carries the wire retryable, but to_exception discards it: it calls Arcp::Errors.for(code, message:, details:), which constructs a fresh error whose retryable? is the class default, ignoring the value the runtime actually sent. So client.get_result and client.request raise exceptions whose retryable? does not reflect the wire.
Verified: a JobError with code: 'TIMEOUT', retryable: false maps to an exception with retryable? == true (the Timeout class default). The README "Error handling" guide explicitly instructs users to branch on e.retryable?; a client will therefore retry an operation the runtime marked non-retryable. Unknown codes are additionally coerced to INTERNAL_ERROR (retryable? == true), turning any unrecognized terminal error into an infinitely-retryable one.
Evidence
# lib/arcp/job/job_error.rb:26-28
def to_exception
Arcp::Errors.for(code, message: message, details: details) # `retryable` dropped
end
# lib/arcp/errors.rb:127-130 — fresh instance, class-default retryable?
def self.for(code, message: nil, details: {})
klass = BY_CODE[code] || Arcp::Errors::Internal
klass.new(message, details: details)
end
Proposed fix
Thread the wire retryable through. Either add a retryable: keyword to Errors.for and have the constructed error override retryable? with the supplied value (falling back to the class default only when absent), or build the exception and set an instance flag consulted by retryable?. Update JobError#to_exception to pass retryable: retryable. Add tests asserting JobError.from_h('code'=>'TIMEOUT','retryable'=>false).to_exception.retryable? == false and the converse.
Acceptance criteria
Category: bug Severity: major
Location:
lib/arcp/job/job_error.rb:26-28(secondary:lib/arcp/client.rb:214,lib/arcp/client.rb:300)Spec: ARCP v1.1 §12
What
Spec §12: "Error payloads carry a
retryableboolean." TheJobErrorvalue object carries the wireretryable, butto_exceptiondiscards it: it callsArcp::Errors.for(code, message:, details:), which constructs a fresh error whoseretryable?is the class default, ignoring the value the runtime actually sent. Soclient.get_resultandclient.requestraise exceptions whoseretryable?does not reflect the wire.Verified: a
JobErrorwithcode: 'TIMEOUT', retryable: falsemaps to an exception withretryable? == true(theTimeoutclass default). The README "Error handling" guide explicitly instructs users to branch one.retryable?; a client will therefore retry an operation the runtime marked non-retryable. Unknown codes are additionally coerced toINTERNAL_ERROR(retryable? == true), turning any unrecognized terminal error into an infinitely-retryable one.Evidence
Proposed fix
Thread the wire
retryablethrough. Either add aretryable:keyword toErrors.forand have the constructed error overrideretryable?with the supplied value (falling back to the class default only when absent), or build the exception and set an instance flag consulted byretryable?. UpdateJobError#to_exceptionto passretryable: retryable. Add tests assertingJobError.from_h('code'=>'TIMEOUT','retryable'=>false).to_exception.retryable? == falseand the converse.Acceptance criteria
retryablevalue, not the class default.retryable? == false.retryablediffers from its class default.