[claude] Add real-time cost ticker for Workshop interactions (#68) (#82)

Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #82.
This commit is contained in:
2026-03-23 20:01:26 +00:00
committed by rockachopa
parent 2fe82988f4
commit 821aa48543
6 changed files with 85 additions and 2 deletions

View File

@@ -15,7 +15,10 @@ export type DebateEvent =
| { type: "debate:argument"; jobId: string; agent: "Beta-A" | "Beta-B"; position: "accept" | "reject"; argument: string }
| { type: "debate:verdict"; jobId: string; accepted: boolean; reason: string };
export type BusEvent = JobEvent | SessionEvent | DebateEvent;
export type CostEvent =
| { type: "cost:update"; jobId: string; sats: number; phase: "eval" | "work" | "session"; isFinal: boolean };
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent;
class EventBus extends EventEmitter {
emit(event: "bus", data: BusEvent): boolean;

View File

@@ -247,6 +247,16 @@ function translateEvent(ev: BusEvent): object | null {
};
}
// ── Real-time cost ticker (#68) ───────────────────────────────────────────
case "cost:update":
return {
type: "cost_update",
jobId: ev.jobId,
sats: ev.sats,
phase: ev.phase,
isFinal: ev.isFinal,
};
default:
return null;
}

View File

@@ -205,6 +205,8 @@ async function runEvalInBackground(
// to avoid economic DoS where pool is reserved before the user ever pays.
eventBus.publish({ type: "job:state", jobId, state: "awaiting_work_payment" });
// Emit estimated cost so the UI ticker can show ~N sats before payment
eventBus.publish({ type: "cost:update", jobId, sats: invoiceSats, phase: "work", isFinal: false });
} else {
await db
.update(jobs)
@@ -307,6 +309,10 @@ async function runWorkInBackground(
refundState,
});
eventBus.publish({ type: "job:completed", jobId, result: workResult.result });
// Emit final actual cost for the UI cost ticker
if (!isFree && actualAmountSats > 0) {
eventBus.publish({ type: "cost:update", jobId, sats: actualAmountSats, phase: "work", isFinal: true });
}
// Credit the generosity pool from paid interactions
if (!isFree && workAmountSats > 0) {

View File

@@ -465,6 +465,11 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
}
});
// Emit real-time cost update for the UI cost ticker (#68)
if (finalState === "complete" && debitedSats > 0) {
eventBus.publish({ type: "cost:update", jobId: requestId, sats: debitedSats, phase: "session", isFinal: true });
}
// ── Trust scoring ────────────────────────────────────────────────────────
if (session.nostrPubkey) {
if (finalState === "complete") {