export declare const WORK_PROPOSAL_SCHEMA_VERSION = 1; /** How often a long-lived store re-sweeps for expired proposals. */ export declare const WORK_PROPOSAL_SWEEP_INTERVAL_MS: number; export type WorkProposalStatus = 'pending' | 'accepted' | 'declined' | 'expired'; export interface WorkProposalRecord { readonly id: string; readonly createdAt: number; readonly expiresAt: number; readonly status: WorkProposalStatus; /** The channel the proposal was sent over, agreement must arrive here. */ readonly surfaceKind: string; readonly surfaceId?: string | undefined; readonly routeId?: string | undefined; readonly externalId?: string | undefined; readonly threadId?: string | undefined; readonly channelId?: string | undefined; readonly userId?: string | undefined; readonly sessionId?: string | undefined; /** The prompt that would be spawned if the owner agrees. */ readonly task: string; /** One short line naming the work, for the proposal message. */ readonly summary: string; /** * Whether the proposal message actually reached the owner's channel. * * A proposal is created before it is sent, and the send can fail (no route * binding, delivery disabled for the surface, no deliverable target, the * transport threw). Until delivery is confirmed the owner has not SEEN this * proposal, so it must not be answerable: {@link WorkProposalStore.listPending} * excludes it, which is what stops an unrelated message from being read as * "yes" to a proposal that was never shown. Fails closed, a record * persisted without the field loads as undelivered. */ readonly delivered: boolean; } export interface WorkProposalReapSummary { readonly expired: number; readonly overCap: number; readonly malformed: number; readonly resolved: number; /** Dropped because the proposal message never reached the owner's channel. */ readonly undelivered: number; readonly total: number; } export declare const EMPTY_WORK_PROPOSAL_REAP_SUMMARY: WorkProposalReapSummary; /** * Validate one persisted entry. Returns null (never throws) for anything that * is not a well-formed proposal, so one bad record cannot take out the file. */ export declare function validateWorkProposal(value: unknown): WorkProposalRecord | null; export interface WorkProposalStoreOptions { /** Absent = memory only (tests, isolated contexts). */ readonly storePath?: string | undefined; readonly maxPending?: number | undefined; readonly now?: (() => number) | undefined; } export interface CreateWorkProposalInput { readonly surfaceKind: string; readonly task: string; readonly summary: string; readonly ttlMs: number; readonly surfaceId?: string | undefined; readonly routeId?: string | undefined; readonly externalId?: string | undefined; readonly threadId?: string | undefined; readonly channelId?: string | undefined; readonly userId?: string | undefined; readonly sessionId?: string | undefined; } export declare class WorkProposalStore { private readonly proposals; private readonly storePath; private readonly maxPending; private readonly now; private sweepTimer; private sweepInterval; private lastReap; private loadMalformed; private writeChain; constructor(options?: WorkProposalStoreOptions); /** * Load, validate, and reap. Fail-safe: an unreadable or corrupt store means * no pending proposals, never a crash, the worst case is that the owner * has to ask again, which is strictly better than answering a bad record. */ init(): Promise; /** * Begin the periodic expiry sweep. Idempotent, and only ever running while * something is actually pending, a daemon with no open proposals carries no * timer at all, and the sweep stops itself once the last one is resolved or * expires. That is what lets the store need no external disposal hook. */ startSweep(intervalMs?: number): void; private hasPending; dispose(): void; create(input: CreateWorkProposalInput): WorkProposalRecord; get(id: string): WorkProposalRecord | null; /** * Confirm that the proposal message reached the owner's channel. Only after * this does the proposal become answerable. */ markDelivered(id: string): WorkProposalRecord | null; /** * The proposal never reached the owner. Drop it rather than leaving a * proposal pending that nobody can answer, and, more importantly, that a * later unrelated message could be matched against. */ markUndeliverable(id: string, reason: string): void; /** * Pending proposals, newest first, optionally narrowed to one surface. * * The reap runs first so an expired proposal is never returned as * answerable, and undelivered proposals are excluded: a proposal the owner * was never shown cannot be the thing their next message was answering. */ listPending(filter?: { readonly surfaceKind?: string | undefined; readonly userId?: string | undefined; }): WorkProposalRecord[]; /** * Resolve a proposal. Returns null when the id is unknown or the proposal is * no longer pending, an already-answered or expired proposal cannot be * answered a second time. */ resolve(id: string, outcome: 'accepted' | 'declined'): WorkProposalRecord | null; /** * Drop expired, resolved, and over-cap records. Pure with respect to I/O, * the caller persists. Safe to call on every read path. */ reap(): WorkProposalReapSummary; /** Cumulative housekeeping disclosure, what was dropped and why. */ disclose(): { readonly pending: number; readonly awaitingDelivery: number; readonly tracked: number; readonly reaped: WorkProposalReapSummary; }; private enforceCap; /** Best-effort durable write, serialized so concurrent resolves cannot interleave. */ private persist; /** Test seam: await any in-flight write. */ flush(): Promise; } //# sourceMappingURL=work-proposal-store.d.ts.map