import type Database from 'better-sqlite3';
import type { Logger } from 'pino';
export type JobKind = 'job' | 'inline';
/** Lifecycle states.
 *  - pending     : created, not yet picked up
 *  - running     : agent currently executing
 *  - completed   : agent finished, result captured; for inline kind the
 *                  reply still has to go through the outbox before terminal
 *                  'delivered' (or 'failed' if the outbox gives up).
 *  - delivered   : (inline only) reply confirmed sent via outbox.
 *  - failed      : agent threw OR outbox gave up
 *  - cancelled   : user-initiated cancel
 *  - interrupted : (Phase 3) running row was killed by agim shutdown — the
 *                  startup scan offers the user a retry.
 *  - replaced    : (Phase 3) interrupted row whose user picked "retry"; the
 *                  new row's id lands in `replaced_by`.
 *  - abandoned   : (Phase 3) interrupted row past the 10-minute window — no
 *                  notification fired, archived for audit.
 */
export type JobStatus = 'pending' | 'running' | 'completed' | 'delivered' | 'failed' | 'cancelled' | 'interrupted' | 'replaced' | 'abandoned';
export interface Job {
    id: number;
    status: JobStatus;
    agent: string;
    prompt: string;
    result: string | null;
    error: string | null;
    created_at: string;
    completed_at: string | null;
    /** IM userId of the originating caller, or '' for legacy / system rows. */
    creator_id: string;
    /** Workspace this job was created under, or '' if unknown. */
    workspace_id: string;
    /** `${platform}:${channelId}:${threadId}` — empty for legacy / system rows. */
    thread_key: string;
    /** 'job' = explicit /job (30d retention), 'inline' = auto-tracked inbound
     *  message (24h retention + Phase 3 lifecycle hooks). */
    kind: JobKind;
    /** ISO when status first transitioned to 'running'. Null for never-run rows. */
    started_at: string | null;
    /** ISO when the outbox confirmed delivery to IM. Inline kind only. */
    delivered_at: string | null;
    /** Id of the new job that replaced this one (Phase 3 retry flow). */
    replaced_by: number | null;
    /** Last outbox row id that tried to deliver this job's result. Reverse
     *  link to `outbox.job_id`. */
    last_outbox_id: number | null;
    /** A2A-L1: id of the inline job whose agent invoked us via
     *  mcp__imhub__call_agent. NULL for user-originated rows. */
    parent_id: number | null;
    /** A2A-L1: how deep in the call chain this row sits. 0 = user;
     *  caller.call_depth + 1 for each A2A hop. Used to enforce
     *  IMHUB_A2A_MAX_DEPTH. */
    call_depth: number;
}
type JobRunner = (job: Job, logger: Logger, signal: AbortSignal) => AsyncGenerator<string>;
/** Phase 3 — shutdown step. Mark every running inline job as 'interrupted'
 *  so the next startup can offer the user a retry. Returns the row count for
 *  logging. Safe to call on a degraded DB (returns 0). */
export declare function markRunningInlineJobsInterrupted(reason?: string): number;
/** Phase 3 — startup scan. Return interrupted inline jobs that finished
 *  within the recovery window (default 10 min via IMHUB_RECOVERY_WINDOW_MS).
 *  Caller iterates the result, sends a per-thread "retry / cancel" prompt,
 *  and tracks the user's reply via the in-memory pending-retry map.
 *
 *  Older interrupted rows are NOT returned — they get swept to 'abandoned'
 *  by sweepAbandonedInterrupted() so they don't pile up in the table. */
export declare function findRecoverableInterrupted(windowMs?: number): Job[];
/** Phase 3 — startup sweep. Any 'interrupted' inline row past the recovery
 *  window transitions to 'abandoned' (preserved for audit but never
 *  notified). Returns the row count. */
export declare function sweepAbandonedInterrupted(windowMs?: number): number;
/** Phase 3 — link an old interrupted row to its replacement once the user
 *  picks "1 重发". The new row's id lands in `replaced_by`; the old row
 *  transitions interrupted → replaced so it stops showing up in any future
 *  recovery scan. */
export declare function markJobReplacedBy(oldId: number, newId: number): void;
/** Phase 3 — user picked "2 取消" on the retry prompt. Interrupted → cancelled. */
export declare function markInterruptedCancelled(id: number): void;
/** Exported for the recovery module so it can use the same window for the
 *  user-facing "10 min 内有效" copy. */
export declare function getRecoveryWindowMs(): number;
/** Test hook: artificially age a row's completed_at (or created_at) by a
 *  SQLite modifier like '-2 hours' or '-40 days'. Production code must NOT
 *  call this; it's the only practical way to exercise retention without
 *  sleeping in tests. */
export declare function _backdateForTests(id: number, modifier: string): void;
/** Stop the retention sweep (used by tests + graceful shutdown). */
export declare function stopJobRetentionSweep(): void;
/** Close the underlying SQLite handle. Called by cli.ts on graceful
 *  shutdown so the WAL is checkpointed before exit (L2). */
export declare function closeJobBoardDb(): void;
/**
 * Filter / scoping options accepted by every owner-sensitive job-board API.
 *
 * - When `creatorId` is set, the query is scoped to rows whose `creator_id`
 *   matches the caller OR whose `creator_id` is '' (legacy / ownerless).
 *   This preserves visibility of pre-migration jobs while preventing one
 *   user from poking at another's freshly-created ones.
 * - When `creatorId` is unset (undefined), no ownership filter is applied
 *   — used by REST / ACP / scheduler entry-points that are guarded by the
 *   web token, not by per-user identity.
 */
export interface OwnerOpts {
    creatorId?: string;
    /** Optional agent-name filter. When set, only rows whose `agent` column
     *  matches are returned. Used by the dashboard to give per-agent views
     *  (claude-code / opencode / codex tabs); IM commands generally don't
     *  pass it (they want the user's full job history). */
    agent?: string;
    /** Optional kind filter. 'job' = explicit /job rows; 'inline' = the
     *  per-message rows auto-created by the cli main loop (Phase 2).
     *  Web /tasks Jobs tab uses this to give the operator a clean
     *  explicit-only vs inline-only view. */
    kind?: JobKind;
}
export interface CreateJobOpts {
    creatorId?: string;
    workspaceId?: string;
    /** 'job' (default) for explicit /job, 'inline' for cli auto-tracking. */
    kind?: JobKind;
    /** `${platform}:${channelId}:${threadId}` — required for inline kind so
     *  Phase 3 can route recovery notifications back to the originating IM
     *  thread. Optional for kind='job' (legacy CLI / scheduler callers). */
    threadKey?: string;
    /** A2A-L1: id of the inline job whose agent invoked this one. NULL on
     *  user-originated rows. */
    parentId?: number | null;
    /** A2A-L1: call depth (caller.call_depth + 1). 0 for user-originated. */
    callDepth?: number;
}
export declare function createJob(agent: string, prompt: string, opts?: CreateJobOpts): number;
/** Inline-job convenience: same as createJob but never throws — returns -1
 *  when the board is disabled / locked. Caller is expected to log + carry
 *  on with the in-memory fast path. The whole tracking machinery short-
 *  circuits when env IMHUB_INLINE_JOB_TRACKING=0.
 *
 *  parentId/callDepth are A2A-L1 hand-offs: when an agent tool-calls
 *  another agent via mcp__imhub__call_agent, the bus routes through here
 *  to record the parent/depth relationship. User-originated rows leave
 *  these undefined → NULL parent, depth=0. */
export declare function createInlineJob(opts: {
    agent: string;
    prompt: string;
    threadKey: string;
    creatorId?: string;
    workspaceId?: string;
    parentId?: number | null;
    callDepth?: number;
}): number;
export declare function getJob(id: number, opts?: OwnerOpts): Job | null;
export declare function listJobs(limit?: number, status?: JobStatus, opts?: OwnerOpts): Job[];
/** Replace the agent column. Used by inline jobs: cli.ts creates the row with
 *  the default agent name before routing resolves the actual agent, then this
 *  fires inside onAgentResolved to correct the record. No-op when status is
 *  past 'running' so we don't rewrite history. */
export declare function updateJobAgent(id: number, agent: string): void;
/** pending → running. Stamps started_at. Idempotent: an already-running row
 *  keeps its original started_at. */
export declare function markJobRunning(id: number): void;
/** running → completed (result captured). For inline kind 'delivered' is the
 *  next hop, triggered by the outbox worker once the IM platform confirms.
 *  For 'job' kind this is the terminal success state (no delivery channel). */
export declare function markJobCompleted(id: number, result: string): void;
/** Link a job row to the outbox row that will (try to) deliver its result.
 *  Worker uses outbox.job_id to find this row; this column is the reverse
 *  link for /job inspection ("which outbox row carried this reply"). */
export declare function linkJobOutbox(id: number, outboxId: number): void;
/** completed → delivered. Called by the outbox worker after the IM platform
 *  ack. No-op if the row isn't in 'completed' (e.g. user cancelled meanwhile). */
export declare function markJobDelivered(id: number): void;
/** Terminal failure (agent threw, outbox gave up, etc.). Captures the error
 *  on the row. completed_at gets stamped so retention sweeps treat the row
 *  the same as a normal terminal state. */
export declare function markJobFailed(id: number, error: string): void;
/**
 * Acquire a job slot. Returns immediately when capacity available, otherwise
 * queues the caller and returns a promise that resolves when a slot frees.
 */
declare function acquireSlot(): Promise<void>;
declare function releaseSlot(): void;
export declare function runJob(id: number, runner: JobRunner, logger: Logger): Promise<void>;
/** Test/diagnostic accessor: how many runJob invocations are currently in flight. */
export declare function _activeJobCount(): number;
/** Test/diagnostic accessor: the wait queue depth. */
export declare function _waitQueueDepth(): number;
/** Test-only: directly exercise the concurrency gate without SQLite. */
export declare const _testSlot: {
    acquire: typeof acquireSlot;
    release: typeof releaseSlot;
};
/** Batch 2 (#2 Stop) — return the most recent 'running' (or 'pending')
 *  job on the given thread, or null if none. Used by `/stop` to find
 *  the in-flight run to abort. */
export declare function findActiveJobByThread(threadKey: string): Job | null;
export declare function cancelJob(id: number, opts?: OwnerOpts): boolean;
/**
 * Delete jobs that finished (completed / failed / cancelled) more than
 * `daysRetention` days ago. 'pending' / 'running' rows are never pruned —
 * a stalled-but-resumable job is the operator's call. Returns the row
 * count so callers / tests can assert.
 */
export declare function pruneOldJobs(d?: Database.Database, daysRetention?: number): number;
export declare function getJobStats(): {
    total: number;
    pending: number;
    running: number;
    completed: number;
    failed: number;
};
export {};
//# sourceMappingURL=job-board.d.ts.map