diff --git a/artifacts/api-server/src/routes/jobs.ts b/artifacts/api-server/src/routes/jobs.ts index dde1590..e3622e9 100644 --- a/artifacts/api-server/src/routes/jobs.ts +++ b/artifacts/api-server/src/routes/jobs.ts @@ -372,29 +372,17 @@ async function advanceJob(job: Job): Promise { reserved: partialGrantReserved, }); if (partialGrantReserved <= 0 && (job.absorbedSats ?? 0) > 0) { - // Pool drained between decide() (advisory) and now. We cannot honour the subsidy. - // Fail the job with a clear message so the user can retry at full price. - // Their payment was for the work portion (chargeSats); Timmy does not absorb - // the difference when the pool is empty — free service pauses as designed. - logger.warn("partial grant pool drained at payment — aborting job", { + // Pool drained between advisory decide() and payment confirmation. + // The user already paid their portion; we cannot abandon them without service. + // Proceed with work. Timmy absorbs a bounded loss (at most job.absorbedSats sats) + // for this edge case. runWorkInBackground will record actualAbsorbed = 0 since + // partialGrantReserved = 0, so no pool accounting error occurs; the debit is + // simply unrecorded (pool was already empty). + logger.warn("partial grant pool drained after payment — work proceeds, no pool debit", { jobId: job.id, pubkey: job.nostrPubkey.slice(0, 8), requestedAbsorb: job.absorbedSats, }); - await db - .update(jobs) - .set({ - state: "failed", - errorMessage: "Generosity pool exhausted — please retry at full price.", - updatedAt: new Date(), - }) - .where(eq(jobs.id, job.id)); - eventBus.publish({ - type: "job:failed", - jobId: job.id, - reason: "Generosity pool exhausted — please retry at full price.", - }); - return getJobById(job.id); } }