Add session mode for pre-funded request processing

Implement session-based API endpoints for creating, managing, and interacting with pre-funded sessions, including deposit and top-up invoice generation, macaroon authentication, and per-request debiting of compute costs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 418bf6f8-212b-4bb0-a7a5-8231a061da4e
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 2dc3847e-7186-4a22-9c7e-16cd31bca8d9
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9f85e954-647c-46a5-90a7-396e495a805a/418bf6f8-212b-4bb0-a7a5-8231a061da4e/sPDHkg8
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
alexpaynex
2026-03-18 20:00:24 +00:00
parent dfc9ecdc7b
commit ab2cc06a79
29 changed files with 1075 additions and 978 deletions

View File

@@ -0,0 +1,39 @@
-- Migration: Session balance mode (Mode 2)
-- Users pre-fund a Lightning session; requests auto-debit actual costs.
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
state TEXT NOT NULL DEFAULT 'awaiting_payment',
balance_sats INTEGER NOT NULL DEFAULT 0,
deposit_amount_sats INTEGER NOT NULL,
deposit_payment_hash TEXT NOT NULL,
deposit_payment_request TEXT NOT NULL,
deposit_paid BOOLEAN NOT NULL DEFAULT false,
topup_amount_sats INTEGER,
topup_payment_hash TEXT,
topup_payment_request TEXT,
topup_paid BOOLEAN,
macaroon TEXT,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS session_requests (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
request TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'processing',
result TEXT,
reason TEXT,
error_message TEXT,
eval_input_tokens INTEGER,
eval_output_tokens INTEGER,
work_input_tokens INTEGER,
work_output_tokens INTEGER,
debited_sats INTEGER,
balance_after_sats INTEGER,
btc_price_usd REAL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View File

@@ -3,3 +3,4 @@ export * from "./invoices";
export * from "./conversations";
export * from "./messages";
export * from "./bootstrap-jobs";
export * from "./sessions";

View File

@@ -0,0 +1,101 @@
import { pgTable, text, timestamp, integer, boolean, real } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
// ── Session state machine ─────────────────────────────────────────────────────
export const SESSION_STATES = [
"awaiting_payment",
"active",
"paused",
"expired",
] as const;
export type SessionState = (typeof SESSION_STATES)[number];
export const SESSION_REQUEST_STATES = [
"processing",
"complete",
"rejected",
"failed",
] as const;
export type SessionRequestState = (typeof SESSION_REQUEST_STATES)[number];
// ── sessions ──────────────────────────────────────────────────────────────────
export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
state: text("state").$type<SessionState>().notNull().default("awaiting_payment"),
balanceSats: integer("balance_sats").notNull().default(0),
depositAmountSats: integer("deposit_amount_sats").notNull(),
// Deposit invoice (stored inline — avoids FK into jobs table)
depositPaymentHash: text("deposit_payment_hash").notNull(),
depositPaymentRequest: text("deposit_payment_request").notNull(),
depositPaid: boolean("deposit_paid").notNull().default(false),
// Current topup invoice (one at a time; all nullable until created)
topupAmountSats: integer("topup_amount_sats"),
topupPaymentHash: text("topup_payment_hash"),
topupPaymentRequest: text("topup_payment_request"),
topupPaid: boolean("topup_paid"),
// Auth token — issued once when session activates; required for requests
macaroon: text("macaroon"),
// TTL — refreshed on each successful request
expiresAt: timestamp("expires_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const insertSessionSchema = createInsertSchema(sessions).omit({
createdAt: true,
updatedAt: true,
});
export type Session = typeof sessions.$inferSelect;
export type InsertSession = z.infer<typeof insertSessionSchema>;
// ── session_requests ──────────────────────────────────────────────────────────
export const sessionRequests = pgTable("session_requests", {
id: text("id").primaryKey(),
sessionId: text("session_id")
.notNull()
.references(() => sessions.id),
request: text("request").notNull(),
state: text("state")
.$type<SessionRequestState>()
.notNull()
.default("processing"),
result: text("result"),
reason: text("reason"),
errorMessage: text("error_message"),
// Eval token usage (Haiku judge)
evalInputTokens: integer("eval_input_tokens"),
evalOutputTokens: integer("eval_output_tokens"),
// Work token usage (Sonnet; null for rejected requests)
workInputTokens: integer("work_input_tokens"),
workOutputTokens: integer("work_output_tokens"),
// Balance debit for this request
debitedSats: integer("debited_sats"),
balanceAfterSats: integer("balance_after_sats"),
btcPriceUsd: real("btc_price_usd"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
});
export const insertSessionRequestSchema = createInsertSchema(sessionRequests).omit({
createdAt: true,
updatedAt: true,
});
export type SessionRequest = typeof sessionRequests.$inferSelect;
export type InsertSessionRequest = z.infer<typeof insertSessionRequestSchema>;