/**
 * Job Scheduler — cron-based job execution engine.
 *
 * Schedules jobs via croner, respects session limits and quota,
 * queues jobs when at capacity, and drains when slots open.
 *
 * Simplified from Dawn's 1400-line scheduler — serial queue,
 * no JSONL discovery, no machine coordination.
 */
import { IntegrationGate } from './IntegrationGate.js';
import { JobRunHistory } from './JobRunHistory.js';
import { SkipLedger } from './SkipLedger.js';
import type { SessionManager } from '../core/SessionManager.js';
import type { StateManager } from '../core/StateManager.js';
import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
import type { CanRunJobResult, IntelligenceProvider, MessagingAdapter } from '../core/types.js';
import type { JobDefinition, JobSchedulerConfig, JobPriority } from '../core/types.js';
import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
import type { JobClaimManager } from './JobClaimManager.js';
import type { JobLeaseClaimStore } from './JobLeaseClaimStore.js';
import { type CutoverGateInput, type CutoverDecision } from './JobLeaseCutoverGate.js';
import type { TopicMemory } from '../memory/TopicMemory.js';
interface QueuedJob {
    slug: string;
    reason: string;
    queuedAt: string;
}
interface SchedulerStatus {
    running: boolean;
    paused: boolean;
    jobCount: number;
    enabledJobs: number;
    queueLength: number;
    activeJobSessions: number;
}
export declare class JobScheduler {
    private config;
    private sessionManager;
    private state;
    private stateDir;
    private skipLedger;
    private runHistory;
    private jobs;
    private cronTasks;
    private queue;
    private running;
    private paused;
    /** Map session names to run IDs for completion tracking */
    private activeRunIds;
    /** Retry state for skipped jobs: slug → { retries, lastAttempt, timer } */
    private retryState;
    /** Retry delays in ms: 1min, 5min, 15min, 30min, 1h, 2h */
    private static readonly RETRY_DELAYS_MS;
    /** Local machine identity — used for machine-scoped job filtering */
    private machineId;
    private machineName;
    /**
     * Callback to check if a job at the given priority may run.
     * May return a plain boolean (legacy) or a CanRunJobResult so the
     * scheduler can record the actual gating reason (memory vs quota vs gate).
     */
    canRunJob: (priority: JobPriority) => boolean | CanRunJobResult;
    /** Optional messenger for sending job notifications */
    private messenger;
    /** Optional Telegram adapter for job-topic coupling */
    private telegram;
    /** Optional quota tracker for death classification cross-reference */
    private quotaTracker;
    /** Optional job claim manager for multi-machine deduplication (Phase 4C) */
    private claimManager;
    /**
     * MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 journal-lease cutover. The durable,
     * epoch-fenced lease store — the journal-backed UPGRADE of the best-effort
     * AgentBus broadcast in `claimManager`. It is consulted for a job ONLY when
     * `journalLeaseGate` selects the 'journal' path (flag on + pool coherent). The
     * gate guarantees the two mechanisms are never both live for a job set (the
     * named migration hazard). When unset, the scheduler stays on the legacy bus
     * path (byte-for-byte today's behavior).
     */
    private leaseClaimStore;
    /**
     * Live provider for the cutover-gate input (flag + dry-run + the pool peers'
     * advertised ws43JournalLease capability + this machine's lease epoch). Read
     * LIVE at each claim boundary so a flag flip / a peer going dark cuts the pool
     * back to the bus immediately. When unset, the gate is never consulted and the
     * legacy bus path runs (strict no-op).
     */
    private journalLeaseGate;
    /** Optional sink for the most-recent cutover decision (status/audit surface). */
    private onCutoverDecision;
    /**
     * UNIFIED-SESSION-LIFECYCLE §P0 #9 (SE-8): function returning cumulative
     * wall-time-asleep in milliseconds during a half-open window [startMs, endMs).
     * Wired by server.ts to SleepWakeDetector.getCumulativeSleepMsBetween. When
     * unset, the wake-reaper falls back to no sleep subtraction (the pre-Phase-2
     * behavior — never more aggressive).
     */
    private cumulativeSleepProvider;
    /** Optional LLM provider for per-job reflection (Living Skills Phase 4) */
    private intelligence;
    /** Optional IntegrationGate for post-completion learning consolidation */
    private integrationGate;
    /** Optional TopicMemory for topic-aware job sessions */
    private topicMemory;
    /**
     * MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 role-guard-at-spawn (CMT-1416).
     * When set, a STATE-WRITING job (JobDefinition.writesState) is refused at the
     * spawn boundary if this machine does NOT hold the lease — the TOCTOU re-check
     * that closes the window where a machine awake at boot demotes to read-only
     * standby mid-run while its cron tasks keep firing (the scheduler is built only
     * when awake but is never torn down on demotion). The provider returns the live
     * gate state read at the spawn boundary (NOT cached at wiring time): `enabled`
     * reflects `multiMachine.seamlessness.ws43RoleGuard` and `holdsLease` reflects
     * the coordinator's structural lease verdict. When unset (or `enabled:false`),
     * the guard is a strict no-op — byte-for-byte today's behavior. A
     * single-machine agent always holds the lease, so the guard never fires.
     */
    private roleGuardProvider;
    /**
     * MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 — best-effort callback to raise ONE
     * deduped attention item when a state-writing job is refused at the spawn
     * boundary. Wired by server.ts to the attention surface. Signal-only: a missing
     * callback (or a throwing one) never blocks the refusal — the refusal is the
     * load-bearing safety, the attention item is the operator heads-up.
     */
    private roleGuardAttention;
    constructor(config: JobSchedulerConfig, sessionManager: SessionManager, state: StateManager, stateDir: string);
    /**
     * Set a messaging adapter for job completion notifications.
     */
    setMessenger(adapter: MessagingAdapter): void;
    /**
     * Inject the cumulative-sleep provider used by the wake-reaper
     * (UNIFIED-SESSION-LIFECYCLE §P0 #9 / SE-8). Pass undefined or omit to leave
     * the reaper in its no-subtraction fallback behavior.
     */
    setCumulativeSleepProvider(fn: (startMs: number, endMs: number) => number): void;
    /**
     * Set the Telegram adapter for job-topic coupling.
     * Every job gets its own topic — the user's window into the job.
     */
    setTelegram(adapter: TelegramAdapter): void;
    /**
     * Set the quota tracker for session death classification.
     * When set, session deaths are cross-referenced with quota state
     * to determine if they died from quota exhaustion.
     */
    setQuotaTracker(tracker: QuotaTracker): void;
    /**
     * Set the job claim manager for multi-machine deduplication.
     * When set, the scheduler will broadcast claims before executing jobs
     * and skip jobs already claimed by other machines.
     */
    setJobClaimManager(manager: JobClaimManager): void;
    /**
     * MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 — inject the journal-lease cutover.
     *
     * `store` is the durable epoch-fenced lease store; `gateInput` returns the
     * LIVE cutover-gate input (flag, dry-run, pool peers' advertised capability,
     * and this machine's current lease epoch) — read fresh at each claim boundary,
     * never cached. `onDecision` (optional) receives the resolved decision per job
     * for the status/audit surface.
     *
     * When omitted, the scheduler never consults the journal path — the legacy
     * AgentBus `claimManager` runs unchanged (single-machine / flag-off no-op).
     */
    setJournalLeaseCutover(store: JobLeaseClaimStore, gateInput: () => CutoverGateInput & {
        epoch: number;
    }, onDecision?: (slug: string, decision: CutoverDecision) => void): void;
    /**
     * Resolve which claim mechanism a job uses RIGHT NOW. Pure read of the live
     * gate input. Returns null when the cutover is not wired (legacy bus only).
     * Exposed for the scheduler's claim seams (check + claim + complete) so a
     * single evaluation drives all three — never two mechanisms for one job.
     */
    private resolveClaimPath;
    /**
     * Release a claim for a completed job across BOTH mechanisms. The path that
     * actually took the claim may differ from the path the gate would pick at
     * completion time (a mid-run flag flip), so we release both — each store's
     * completeClaim is a strict no-op when this machine does not own the slug's
     * live record, so releasing the unused store is harmless. The journal release
     * is synchronous (durable); the bus release is best-effort async.
     */
    private releaseClaim;
    /**
     * Set local machine identity for machine-scoped job filtering.
     * Jobs with a `machines` field will only run on machines whose ID or name matches.
     */
    setMachineIdentity(machineId: string, machineName: string): void;
    /**
     * Set the intelligence provider for per-job LLM reflection (Living Skills Phase 4).
     */
    setIntelligence(provider: IntelligenceProvider): void;
    /**
     * Set the IntegrationGate for post-completion learning consolidation.
     * When set, reflection runs synchronously before queue drain.
     * When not set, the existing fire-and-forget reflection behavior is preserved.
     */
    setIntegrationGate(gate: IntegrationGate): void;
    /**
     * Set the TopicMemory for topic-aware job sessions.
     * When set, jobs bound to a topic receive awareness context about the topic's focus.
     */
    setTopicMemory(topicMemory: TopicMemory): void;
    /**
     * MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.3 — inject the role-guard-at-spawn
     * provider. `provider` is read LIVE at each spawn boundary (never cached) so a
     * mid-run demotion takes effect immediately. `onRefused` (optional) raises the
     * deduped attention item when a state-writing job is refused. When the provider
     * is omitted the guard is a strict no-op. See `roleGuardProvider`.
     */
    setRoleGuard(provider: () => {
        enabled: boolean;
        holdsLease: boolean;
    }, onRefused?: (slug: string, machineId: string | null) => void): void;
    /**
     * Start the scheduler — load jobs, set up cron tasks, check for missed jobs.
     */
    start(): void;
    /**
     * Stop the scheduler — cancel all cron tasks.
     */
    stop(): void;
    /**
     * Trigger a job by slug. Checks claims, quota, session limits, queues if at capacity.
     */
    triggerJob(slug: string, reason: string): Promise<'triggered' | 'queued' | 'skipped'>;
    /**
     * Check if a job is scoped to run on this machine.
     * Jobs without a `machines` field run everywhere (backwards-compatible).
     * Jobs with `machines` only run if this machine's ID or name matches.
     */
    private isJobScopedToThisMachine;
    /**
     * Process the queue — dequeue and run next job if a slot is available.
     */
    processQueue(): void;
    /**
     * Pause — cron tasks keep ticking but triggers are skipped.
     */
    pause(): void;
    /**
     * Clear the pending job queue.
     */
    clearQueue(): void;
    /**
     * Resume — triggers start executing again.
     */
    resume(): void;
    /**
     * Get scheduler status for the /status endpoint.
     */
    getStatus(): SchedulerStatus;
    /**
     * Get loaded job definitions (for /jobs endpoint).
     */
    getJobs(): JobDefinition[];
    /**
     * Get the current queue.
     */
    getQueue(): QueuedJob[];
    /**
     * Get live nextScheduled times for all jobs from croner tasks.
     * Returns a map of slug → ISO timestamp (or undefined if no cron task).
     * This is the live schedule, independent of saved state files.
     */
    getNextRunTimes(): Record<string, string | undefined>;
    /**
     * Check if a job will run on this machine (for API visibility).
     */
    isJobLocal(slug: string): boolean;
    /**
     * Get the skip ledger instance (for API routes).
     */
    getSkipLedger(): SkipLedger;
    /**
     * Get the run history instance (for API routes).
     */
    getRunHistory(): JobRunHistory;
    /**
     * Reap stuck job runs after a system sleep/wake transition.
     *
     * When the host machine sleeps mid-job, the job's tmux session is
     * suspended along with everything else and timer-based supervision
     * (claim TTL, completion callbacks) cannot fire. On wake, runs that
     * were in-flight stay `pending` in the ledger until the next claim
     * TTL multi-hour timeout kicks in — or, in some cases, indefinitely.
     *
     * This method is invoked from the SleepWakeDetector wake handler.
     * It scans `activeRunIds` and reaps any run whose elapsed time
     * exceeds twice its `expectedDurationMinutes` — the same threshold
     * the scheduler already uses for claim TTL.
     *
     * Reaping is idempotent: a second invocation finds the run is no
     * longer `pending` and skips it.
     */
    reapStuckRuns(sleepEvent: {
        sleepDurationSeconds: number;
    }): Promise<{
        reaped: string[];
        skipped: number;
    }>;
    private enqueue;
    /**
     * Execute a `script`-type job directly in a bounded subprocess instead of
     * spawning a model session. Script jobs are zero-token infrastructure work
     * (e.g. dashboard-link refresh); spawning a model for them wasted a session
     * slot and could hang for hours (Codey gap-run F005). Mirrors spawnJobSession's
     * run-history / job-state / claim bookkeeping so observability is identical.
     */
    private runScriptJob;
    private spawnJobSession;
    /**
     * Resolve the model tier for a job. Normally uses the job's configured model,
     * but gates can write a severity file to escalate the model for complex work.
     *
     * The git-sync gate writes to /tmp/instar-git-sync-severity:
     * - "clean" → use configured model (haiku)
     * - "state" → escalate to sonnet (structured conflict resolution)
     * - "code"  → escalate to opus (semantic code merge)
     */
    private resolveModelTier;
    private buildPrompt;
    /**
     * Resolve the effective notification mode for a job.
     * Default (undefined) → 'on-alert': quiet unless signaled.
     */
    private getNotifyMode;
    /**
     * Check if session output contains an attention signal.
     * The convention: [ATTENTION] on its own line (case-insensitive).
     */
    private hasAttentionSignal;
    /**
     * Extract handoff notes from session output.
     * Agents can include a [HANDOFF] ... [/HANDOFF] block in their output
     * to leave context for the next execution. This is auto-extracted on completion.
     */
    static extractHandoff(output: string): string | null;
    private getConsecutiveFailures;
    private getNextRun;
    /**
     * Called when a job's session completes. Updates job state and notifies via messenger.
     */
    notifyJobComplete(sessionId: string, tmuxSession: string): Promise<void>;
    /**
     * Ensure every enabled job has a Telegram topic.
     * Creates topics for jobs that don't have one.
     * This is the "job-topic coupling" — every job lives in a topic.
     */
    private ensureJobTopics;
    /**
     * Save a job-topic mapping (used when recreating a deleted topic).
     */
    private saveJobTopicMapping;
    /**
     * Run a job's gate command asynchronously. Returns true if the job should proceed, false to skip.
     * Gates are zero-token pre-screening — a bash command that exits 0 (proceed) or non-zero (skip).
     * Retries up to 3 times with 5s delay to handle transient failures (e.g., server restart windows).
     *
     * Uses non-blocking async execution to avoid stalling the Node.js event loop while gates run.
     * Synchronous gate execution was causing health-check timeouts under startup load (many gates
     * firing concurrently after a restart would block the event loop for 30-300+ seconds).
     */
    private runGateAsync;
    /**
     * Normalize the canRunJob callback's result. Older wrappers return a bare
     * boolean; newer ones return { allowed, reason, detail } so the scheduler
     * can record WHY a job was gated (memory pressure vs quota etc.).
     */
    private normalizeCanRunResult;
    /**
     * Schedule a retry for a transiently-skipped job.
     * Uses escalating delays: 1min, 5min, 15min, 30min, 1h, 2h.
     * Gives up after exhausting all retry slots within a single scheduled window.
     */
    private scheduleRetry;
    /**
     * Clear retry state for a job (on success or new cron window).
     */
    private clearRetryState;
    /**
     * Alert when a job hits consecutive failure thresholds.
     * Critical/high priority jobs alert after 2 failures.
     * Medium/low priority jobs alert after 3 failures.
     * Only alerts at the threshold (not every failure after).
     *
     * When the failure is a session limit issue (not a job execution error),
     * the notification is reframed as "Job Blocked" with intelligent diagnostics:
     * running session list with ages, stale session flags, memory pressure,
     * and actionable suggestions.
     */
    private alertOnConsecutiveFailures;
    /**
     * Build an intelligent "Job Blocked" notification when a job can't start
     * because all session slots are occupied. Includes session diagnostics,
     * staleness detection, memory pressure, and actionable suggestions.
     */
    private buildSessionBlockedAlert;
    private checkMissedJobs;
    /**
     * Run per-job LLM reflection after execution.
     * Always-on for every completed job — history is memory.
     * Persists the reflection to run history and optionally sends to Telegram.
     */
    private runJobReflection;
    /**
     * Resolve a per-job tool allowlist per INSTAR-JOBS-AS-AGENTMD spec §5.
     *
     * Decision matrix:
     *   | execute.type !== 'agentmd'                       → kind:'legacy', allowlist:null (full tools, back-compat)
     *   | frontmatter.toolAllowlist = string[]             → kind:'array', allowlist:[...]
     *   | frontmatter.toolAllowlist = "*" + unrestricted   → kind:'unrestricted', allowlist:"*", unrestrictedTools:true
     *   | frontmatter.toolAllowlist = "*" + NOT authorized → kind:'clamped', allowlist:['Read'], clampedAllowlist:true (warning event emitted by caller)
     *   | frontmatter.toolAllowlist missing + origin=user  → kind:'default-user', allowlist:['Read']
     *   | frontmatter.toolAllowlist missing + origin=instar → kind:'instar-no-allowlist', allowlist:null (degradation event; Phase 1c lock-file will close)
     *
     * Returns a single object describing the resolution so callers can both
     * (a) thread the right argument into the spawn call and (b) annotate the
     * run record. Side-effects (degradation events) are NOT performed here —
     * they happen in the caller, where the scheduler context is available.
     */
    static resolveAllowlist(job: JobDefinition): AllowlistResolution;
    /** Classify a lockTrust value as a real-tamper signal. The transitional
     *  `untrusted-no-lockfile` is intentionally NOT a tamper signal — until
     *  Phase 1c-build ships, every instar agent carries this value on every
     *  origin:instar entry; treating it as tamper would break the rollout. */
    static isLockUntrustedTamper(lockTrust: JobDefinition['lockTrust']): boolean;
    /**
     * Compute Phase 1b run-record observability fields for a job. Pure
     * function: hashing is deterministic over body + canonicalized
     * frontmatter; non-agentmd entries carry nulls.
     */
    static computeRunObservability(job: JobDefinition, resolution: AllowlistResolution): RunObservability;
    /**
     * Canonical JSON encoding for hash stability. Sorts object keys at every
     * depth so the same logical frontmatter always produces the same hash,
     * regardless of insertion order. Arrays preserve order. Primitive nulls
     * and undefined are encoded the same way `JSON.stringify` does.
     */
    static canonicalize(value: unknown): string;
    /**
     * Emit Phase 1b side-channel signals for an allowlist resolution.
     * Side effects (Dashboard event + degradation event) are gathered here
     * so spawnJobSession can call once at spawn time. Pure function on the
     * scheduler's `state.appendEvent` and the global DegradationReporter.
     *
     * The clamped-allowlist warning is a Dashboard-visible event AND a
     * DegradationReporter event — both surface to the user via different
     * channels (event stream vs degradation digest).
     *
     * The instar-no-allowlist path is a DegradationReporter event only:
     * Phase 1c's lock-file defaults will close this gap; until then the
     * temporary "full tools" behavior is documented and observable.
     */
    private emitAllowlistSignals;
    /** Sync in-memory runtime read for the GuardRegistry (GET /guards).
     *  Registration is not life: a load-shed pause reads enabled:false
     *  (off-runtime-divergent against an on-config), never healthy. Cheap
     *  property reads ONLY — getStatus() lists sessions and must not be
     *  called from the guards route. */
    guardStatus(): {
        enabled: boolean;
        jobCount: number;
        pausedJobCount: number;
    };
}
/** Result of {@link JobScheduler.resolveAllowlist}. Pure-data; no state. */
export interface AllowlistResolution {
    kind: 'legacy' | 'array' | 'unrestricted' | 'clamped' | 'default-user' | 'instar-no-allowlist' | 'lock-untrusted-clamped';
    /** What the resolver will pass to the spawn:
     *  - `string[]` → spawn receives `--allowedTools <comma-separated>`
     *  - `"*"` → spawn omits `--allowedTools` (full tools authorized)
     *  - `null` → spawn omits `--allowedTools` (legacy or instar-no-allowlist fallback) */
    allowlist: string[] | '*' | null;
    unrestrictedTools: boolean;
    clampedAllowlist: boolean;
    /** True when an `origin:instar` agentmd job had its tool elevation refused
     *  because lockTrust is in a real-tamper state (`bad-signature`,
     *  `not-in-lockfile`, or `hash-mismatch`). The transitional
     *  `untrusted-no-lockfile` state does NOT trigger this clamp — see
     *  {@link JobScheduler.resolveAllowlist} for the rationale. */
    lockUntrustedClamp?: boolean;
}
/** Phase 1b run-record observability payload. */
export interface RunObservability {
    origin: 'instar' | 'user' | 'legacy';
    resolvedPath: string | null;
    bodyHash: string | null;
    frontmatterHash: string | null;
    manifestVersion: number | null;
    toolAllowlist: string[] | '*' | null;
    unrestrictedTools: boolean;
    clampedAllowlist: boolean;
}
export {};
//# sourceMappingURL=JobScheduler.d.ts.map