Make job evaluation and execution run in the background
Refactors `runEvalInBackground` and `runWorkInBackground` to execute AI tasks asynchronously. Updates `pollJob` in `ui.ts` to handle 'evaluating', 'executing', and 'failed' states, and corrects `data.status` to `data.state` and `data.rejectionReason` to `data.reason`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: ecf857ee-fa4d-47db-b4c1-b374ffb3815d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/Q83Uqvu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -19,9 +19,112 @@ async function getInvoiceById(id: string) {
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the AI eval in a background task (fire-and-forget) so HTTP polls
|
||||
* return immediately with "evaluating" state instead of blocking 5-8 seconds.
|
||||
*/
|
||||
async function runEvalInBackground(jobId: string, request: string): Promise<void> {
|
||||
try {
|
||||
const evalResult = await agentService.evaluateRequest(request);
|
||||
|
||||
if (evalResult.accepted) {
|
||||
const inputEst = pricingService.estimateInputTokens(request);
|
||||
const outputEst = pricingService.estimateOutputTokens(request);
|
||||
const breakdown = await pricingService.calculateWorkFeeSats(
|
||||
inputEst,
|
||||
outputEst,
|
||||
agentService.workModel,
|
||||
);
|
||||
|
||||
const workInvoiceData = await lnbitsService.createInvoice(
|
||||
breakdown.amountSats,
|
||||
`Work fee for job ${jobId}`,
|
||||
);
|
||||
const workInvoiceId = randomUUID();
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(invoices).values({
|
||||
id: workInvoiceId,
|
||||
jobId,
|
||||
paymentHash: workInvoiceData.paymentHash,
|
||||
paymentRequest: workInvoiceData.paymentRequest,
|
||||
amountSats: breakdown.amountSats,
|
||||
type: "work",
|
||||
paid: false,
|
||||
});
|
||||
await tx
|
||||
.update(jobs)
|
||||
.set({
|
||||
state: "awaiting_work_payment",
|
||||
workInvoiceId,
|
||||
workAmountSats: breakdown.amountSats,
|
||||
estimatedCostUsd: breakdown.estimatedCostUsd,
|
||||
marginPct: breakdown.marginPct,
|
||||
btcPriceUsd: breakdown.btcPriceUsd,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(jobs.id, jobId));
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, jobId));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Evaluation error";
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, jobId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the AI work execution in a background task so HTTP polls return fast.
|
||||
*/
|
||||
async function runWorkInBackground(jobId: string, request: string, workAmountSats: number, btcPriceUsd: number | null): Promise<void> {
|
||||
try {
|
||||
const workResult = await agentService.executeWork(request);
|
||||
|
||||
const actualCostUsd = pricingService.calculateActualCostUsd(
|
||||
workResult.inputTokens,
|
||||
workResult.outputTokens,
|
||||
agentService.workModel,
|
||||
);
|
||||
|
||||
const lockedBtcPrice = btcPriceUsd ?? 100_000;
|
||||
const actualAmountSats = pricingService.calculateActualChargeSats(actualCostUsd, lockedBtcPrice);
|
||||
const refundAmountSats = pricingService.calculateRefundSats(workAmountSats, actualAmountSats);
|
||||
const refundState = refundAmountSats > 0 ? "pending" : "not_applicable";
|
||||
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({
|
||||
state: "complete",
|
||||
result: workResult.result,
|
||||
actualInputTokens: workResult.inputTokens,
|
||||
actualOutputTokens: workResult.outputTokens,
|
||||
actualCostUsd,
|
||||
actualAmountSats,
|
||||
refundAmountSats,
|
||||
refundState,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(jobs.id, jobId));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Execution error";
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, jobId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the active invoice for a job has been paid and, if so,
|
||||
* advances the state machine. Returns the refreshed job after any transitions.
|
||||
* advances the state machine. AI work runs in the background so this
|
||||
* returns quickly. Returns the refreshed job after any DB transitions.
|
||||
*/
|
||||
async function advanceJob(job: Job): Promise<Job | null> {
|
||||
if (job.state === "awaiting_eval_payment" && job.evalInvoiceId) {
|
||||
@@ -46,60 +149,8 @@ async function advanceJob(job: Job): Promise<Job | null> {
|
||||
|
||||
if (!advanced) return getJobById(job.id);
|
||||
|
||||
try {
|
||||
const evalResult = await agentService.evaluateRequest(job.request);
|
||||
|
||||
if (evalResult.accepted) {
|
||||
const inputEst = pricingService.estimateInputTokens(job.request);
|
||||
const outputEst = pricingService.estimateOutputTokens(job.request);
|
||||
const breakdown = await pricingService.calculateWorkFeeSats(
|
||||
inputEst,
|
||||
outputEst,
|
||||
agentService.workModel,
|
||||
);
|
||||
|
||||
const workInvoiceData = await lnbitsService.createInvoice(
|
||||
breakdown.amountSats,
|
||||
`Work fee for job ${job.id}`,
|
||||
);
|
||||
const workInvoiceId = randomUUID();
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(invoices).values({
|
||||
id: workInvoiceId,
|
||||
jobId: job.id,
|
||||
paymentHash: workInvoiceData.paymentHash,
|
||||
paymentRequest: workInvoiceData.paymentRequest,
|
||||
amountSats: breakdown.amountSats,
|
||||
type: "work",
|
||||
paid: false,
|
||||
});
|
||||
await tx
|
||||
.update(jobs)
|
||||
.set({
|
||||
state: "awaiting_work_payment",
|
||||
workInvoiceId,
|
||||
workAmountSats: breakdown.amountSats,
|
||||
estimatedCostUsd: breakdown.estimatedCostUsd,
|
||||
marginPct: breakdown.marginPct,
|
||||
btcPriceUsd: breakdown.btcPriceUsd,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(jobs.id, job.id));
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "rejected", rejectionReason: evalResult.reason, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, job.id));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Evaluation error";
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, job.id));
|
||||
}
|
||||
// Fire AI eval in background — poll returns immediately with "evaluating"
|
||||
setImmediate(() => { void runEvalInBackground(job.id, job.request); });
|
||||
|
||||
return getJobById(job.id);
|
||||
}
|
||||
@@ -126,51 +177,8 @@ async function advanceJob(job: Job): Promise<Job | null> {
|
||||
|
||||
if (!advanced) return getJobById(job.id);
|
||||
|
||||
try {
|
||||
const workResult = await agentService.executeWork(job.request);
|
||||
|
||||
// ── Honest post-work accounting ───────────────────────────────────────
|
||||
const actualCostUsd = pricingService.calculateActualCostUsd(
|
||||
workResult.inputTokens,
|
||||
workResult.outputTokens,
|
||||
agentService.workModel,
|
||||
);
|
||||
|
||||
// Re-use the BTC price locked at invoice time so sats arithmetic is
|
||||
// consistent. Fall back to the cached oracle price only if the column
|
||||
// is somehow null (should not happen in normal flow).
|
||||
const lockedBtcPrice = job.btcPriceUsd ?? 100_000;
|
||||
const actualAmountSats = pricingService.calculateActualChargeSats(
|
||||
actualCostUsd,
|
||||
lockedBtcPrice,
|
||||
);
|
||||
const refundAmountSats = pricingService.calculateRefundSats(
|
||||
job.workAmountSats ?? 0,
|
||||
actualAmountSats,
|
||||
);
|
||||
const refundState = refundAmountSats > 0 ? "pending" : "not_applicable";
|
||||
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({
|
||||
state: "complete",
|
||||
result: workResult.result,
|
||||
actualInputTokens: workResult.inputTokens,
|
||||
actualOutputTokens: workResult.outputTokens,
|
||||
actualCostUsd,
|
||||
actualAmountSats,
|
||||
refundAmountSats,
|
||||
refundState,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(jobs.id, job.id));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Execution error";
|
||||
await db
|
||||
.update(jobs)
|
||||
.set({ state: "failed", errorMessage: message, updatedAt: new Date() })
|
||||
.where(eq(jobs.id, job.id));
|
||||
}
|
||||
// Fire AI work in background — poll returns immediately with "executing"
|
||||
setImmediate(() => { void runWorkInBackground(job.id, job.request, job.workAmountSats ?? 0, job.btcPriceUsd); });
|
||||
|
||||
return getJobById(job.id);
|
||||
}
|
||||
|
||||
@@ -440,14 +440,28 @@ router.get("/ui", (_req, res) => {
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const r = await fetch(BASE + '/api/jobs/' + jobId);
|
||||
if (!r.ok) return; // keep polling on transient errors
|
||||
const data = await r.json();
|
||||
const s = data.status;
|
||||
const s = data.state;
|
||||
|
||||
// Failed is terminal regardless of phase
|
||||
if (s === 'failed') {
|
||||
clearInterval(pollTimer);
|
||||
hide('card-eval');
|
||||
hide('card-work');
|
||||
$('rejected-reason').textContent = 'Error: ' + (data.errorMessage || 'Something went wrong. Try again.');
|
||||
show('card-rejected');
|
||||
setStep('rejected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === 'eval') {
|
||||
// evaluating = AI running in background, keep waiting
|
||||
if (s === 'evaluating') return;
|
||||
if (s === 'rejected') {
|
||||
clearInterval(pollTimer);
|
||||
hide('card-eval');
|
||||
$('rejected-reason').textContent = data.rejectionReason || 'Request was rejected.';
|
||||
$('rejected-reason').textContent = data.reason || 'Request was rejected.';
|
||||
show('card-rejected');
|
||||
setStep('rejected');
|
||||
} else if (s === 'awaiting_work_payment') {
|
||||
@@ -461,20 +475,17 @@ router.get("/ui", (_req, res) => {
|
||||
setStep('awaiting_work_payment');
|
||||
}
|
||||
} else if (phase === 'work') {
|
||||
// executing = AI running in background, keep waiting
|
||||
if (s === 'executing') return;
|
||||
if (s === 'complete') {
|
||||
clearInterval(pollTimer);
|
||||
hide('card-work');
|
||||
$('result-text').textContent = data.result;
|
||||
show('card-result');
|
||||
setStep('complete');
|
||||
} else if (s === 'failed') {
|
||||
clearInterval(pollTimer);
|
||||
hide('card-work');
|
||||
$('rejected-reason').textContent = 'Job failed: ' + (data.error || 'unknown error');
|
||||
show('card-rejected');
|
||||
}
|
||||
}
|
||||
} catch(e) { /* keep polling */ }
|
||||
} catch(e) { /* keep polling through network errors */ }
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user