import type Database from 'better-sqlite3'; import type { Logger } from 'pino'; /** Bind a controller to a tracked job, including inline jobs that do not run * through runJob(). The owner must call the returned cleanup after its work * has fully settled; cancelJob only aborts and never removes another owner's * binding. */ export declare function bindJobController(id: number, controller: AbortController): () => void; 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__agim__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 * AGIM_A2A_MAX_DEPTH. */ call_depth: number; } type JobRunner = (job: Job, logger: Logger, signal: AbortSignal) => AsyncGenerator; /** 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 AGIM_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 AGIM_INLINE_JOB_TRACKING=0. * * parentId/callDepth are A2A-L1 hand-offs: when an agent tool-calls * another agent via mcp__agim__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. Returns false only when a tracked row is * missing or already terminal (notably cancelled while queued). */ export declare function markJobRunning(id: number): boolean; /** 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; declare function releaseSlot(): void; export declare function runJob(id: number, runner: JobRunner, logger: Logger): Promise; /** 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 | null, daysRetention?: number): number; export declare function getJobStats(): { total: number; pending: number; running: number; completed: number; failed: number; }; export {}; //# sourceMappingURL=job-board.d.ts.map