import type { ForgeProgressSnapshot } from '../forge/progress.js'; import type { ForgeLayoutMap } from '@bitmagic/world-forger/pipeline/index.js'; /** Which command is running. Shown as-is in the dev view, so these are creator-facing words. */ export type JobKind = 'skybox' | 'background' | 'block-type' | 'sound' | 'music' | 'image' | 'character' | 'animation' | 'animation-video' | 'video' | 'prop' | 'vehicle' | 'forge' | 'cover' | 'reference' | 'hq' | 'judge' | 'upload' | 'model'; export interface JobRecord { jobId: string; kind: JobKind; /** What the creator asked for — a prompt or a description. Trimmed for display, not parsed. */ label: string; /** Known up front only for the kinds that upgrade an asset in place (prop, vehicle, hq). */ assetId?: string; status: 'running' | 'failed'; /** The generation's most recent progress line. See `withJobRecord`. */ message: string; /** * Structured progress, for the one kind that has any: a forge's five steps, how far through the * current one is, and what the level being built contains. * * Optional because it is a forge-only enrichment of {@link message}, never a replacement for it — * every other kind has a single sentence and nothing more to say, and a reader (a `bitmagic dev` * from an older install, or the creator's agent reading this directory by hand) must still make * sense of a record without it. */ progress?: ForgeProgressSnapshot; /** * A top-down map of the level is available, at {@link jobLayoutPath}. * * A flag rather than the map itself, because this record is rewritten in full on every progress * tick and re-served to a 1 Hz poller: ~25KB of raster here would be tens of megabytes of disk * writes and HTTP responses across one thirty-minute forge, for a picture that never changes * after the moment it arrives. The reader fetches it once and caches it. */ hasLayout?: boolean; pid: number; startedAt: string; updatedAt: string; error?: string; } export declare const JOBS_DIR: string; export declare function jobsDirPath(root: string): string; /** * Store a job's map. Best-effort like everything else here: a forge that cannot write its picture * is a forge with no picture, never a failed forge. */ export declare function writeJobLayout(root: string, jobId: string, layout: ForgeLayoutMap): boolean; /** Read a job's map, or null when it has none (every job but a forge, and a forge mid-step 2). */ export declare function readJobLayout(root: string, jobId: string): ForgeLayoutMap | null; /** * Every job this project knows about, newest first, with dead `running` records reported as * failed rather than left spinning in the dev view forever. * * Unreadable and unparseable files are skipped, not thrown: a record being written right now is a * normal thing for a directory that several processes append to, and losing one tick of one job is * not worth failing a read that also carries five healthy ones. */ export declare function readJobs(root: string): JobRecord[]; /** * What the dev view shows: the generations still working. * * A failed job leaves the panel exactly the way a successful one does — by disappearing — and the * difference between them shows up where it means something to the person seeing it. The creator * gets an asset, or does not; the agent gets the reason, on the terminal where it can act on it. * See the header for why the reason does not belong in both places. * * Dead `running` records drop out here too, because `readJobs` reports them as failed. That is what * stops a SIGKILLed generation spinning in the panel for the rest of the session. */ export declare function readRunningJobs(root: string): JobRecord[]; /** * Drop failed records older than a day. Called by `dev` at startup rather than on a timer: this is * housekeeping, and the moment someone opens the dev view is the moment a day-old failure stops * being worth showing. */ export declare function pruneJobs(root: string, now?: number): number; export interface JobHandle { readonly jobId: string; /** Record the latest progress line. */ update(message: string): void; /** Record structured progress alongside the line. Only a forge has any; see {@link JobRecord}. */ setProgress(progress: ForgeProgressSnapshot): void; /** Store the job's map to its sidecar and flag the record so a reader knows to fetch it. */ setLayout(layout: ForgeLayoutMap): void; } /** What a body reports structured progress through. A no-op when the record could not be started. */ export interface JobProgressReporter { (progress: ForgeProgressSnapshot): void; /** The map, which is written once and never rides the record. See {@link JobRecord.hasLayout}. */ layout(layout: ForgeLayoutMap): void; } export interface StartJobOptions { root: string; kind: JobKind; label: string; assetId?: string; } export declare function startJob(options: StartJobOptions): JobHandle; /** * Run `body` with its progress recorded for anyone watching. * * The `log` handed to the body is the caller's own logger with a tap on it, so every line a command * already prints also becomes the job's current message. That is deliberate reuse rather than a * second progress vocabulary to keep in step — the dev sidecar's in-process HQ job has worked this * way since it shipped, and it means a command gains live progress in the dev view without a single * new call site inside it. * * The SECOND argument is for the one command that has more than a sentence to say. A forge runs * for half an hour across five steps on two machines, and a line-at-a-time record can only ever * show the most recent one — so `forge` also reports a snapshot of all five (see * `forge/progress.ts`). Every other caller ignores this parameter, which is the point: the log tap * above stays the default, and structured progress is opt-in for the kind that needs it. * * **Never fails the generation it is watching.** Bookkeeping that breaks a paid, minutes-long * generation would be a far worse bug than the missing progress line it is trying to provide, so * every filesystem error here is swallowed. The body's own errors propagate untouched, after being * recorded. */ export declare function withJobRecord(options: StartJobOptions & { log: (message: string) => void; }, body: (log: (message: string) => void, report: JobProgressReporter) => Promise): Promise; /** Success: the asset is in world.json, and that is the record. */ export declare function removeJob(root: string, jobId: string): void; /** Failure: nothing else records it, so the record stays and carries the reason. */ export declare function failJob(root: string, jobId: string, error: string): void;