import { resolveSessionCompactionPolicy } from "@rudderhq/agent-runtime-utils";
import { agents, authUsers, issueComments, issues } from "@rudderhq/db";
import { and, eq, inArray } from "drizzle-orm";
import path from "node:path";
import { getServerAdapter } from "../../agent-runtimes/index.js";
import { asNumber, parseObject } from "../../agent-runtimes/utils.js";
import { resolveDefaultAgentWorkspaceDir, } from "../../home-paths.js";
export { prioritizeProjectWorkspaceCandidatesForRun } from "../agent-run-context.js";
import * as heartbeatCore from "./heartbeat.core.js";
const { MAX_LIVE_LOG_CHUNK_BYTES, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT, HEARTBEAT_MAX_CONCURRENT_RUNS_MIN, HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, DEFERRED_WAKE_CONTEXT_KEY, DETACHED_PROCESS_ERROR_CODE, ORPHANED_PROCESS_TERMINATION_GRACE_MS, ORPHANED_PROCESS_KILL_WAIT_MS, ORPHANED_PROCESS_POLL_INTERVAL_MS, startLocksByAgent, MAX_RECOVERY_CHAIN_DEPTH, ISSUE_PASSIVE_FOLLOWUP_REASON, ISSUE_PASSIVE_FOLLOWUP_WAKE_SOURCE, ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON, ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS, ISSUE_REVIEW_CLOSEOUT_REASON, ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON, ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS, ISSUE_PASSIVE_FOLLOWUP_COOLDOWN_MS_BY_ATTEMPT, ISSUE_PASSIVE_FOLLOWUP_TIMER_CONTINUITY_MAX_WINDOW_MS, SESSIONED_LOCAL_ADAPTERS, heartbeatRunListColumns, appendExcerpt, appendTranscriptEntriesFromChunk, normalizeMaxConcurrentRuns, withAgentStartLock, readNonEmptyString, resolveHeartbeatObservabilitySurface, buildHeartbeatObservationName, compactTraceText, buildIssueRunTraceName, buildHeartbeatRuntimeTraceMetadata, buildHeartbeatAdapterInvokePayload, buildRecentDateKeys, buildDateKeysBetween, fallbackSkillLabel, normalizeLoadedSkill, normalizeLoadedSkillForPayload, emptySkillEvidenceCounts, incrementSkillEvidenceCount, strongestSkillEvidence, resolveSkillEvidence, readSkillEvidenceFromPayload, extractSkillSlugFromPath, collectSkillPathsFromText, collectStringValues, normalizeSkillUseFromPath, dedupeSkillUses, collectSkillUsesFromText, readToolCommandInput, isCommandTranscriptTool, isReadTranscriptTool, inferUsedSkillsFromTranscript, normalizeSkillCandidate, addSkillCandidate, readSkillReferenceSlug, collectSkillReferences, inferUsedSkillsFromPrompt, normalizeLedgerBillingType, resolveLedgerBiller, normalizeBilledCostCents, resolveLedgerScopeForRun } = heartbeatCore;
export function buildExplicitResumeSessionOverride(input) {
    const desiredDisplayId = truncateDisplayId(input.resumeRunSessionIdAfter ?? input.resumeRunSessionIdBefore);
    const taskSessionParams = normalizeSessionParams(input.sessionCodec.deserialize(input.taskSession?.sessionParamsJson ?? null));
    const taskSessionDisplayId = truncateDisplayId(input.taskSession?.sessionDisplayId ??
        (input.sessionCodec.getDisplayId ? input.sessionCodec.getDisplayId(taskSessionParams) : null) ??
        readNonEmptyString(taskSessionParams?.sessionId));
    const canReuseTaskSessionParams = input.taskSession != null &&
        (input.taskSession.lastRunId === input.resumeFromRunId ||
            (!!desiredDisplayId && taskSessionDisplayId === desiredDisplayId));
    const sessionParams = canReuseTaskSessionParams
        ? taskSessionParams
        : desiredDisplayId
            ? { sessionId: desiredDisplayId }
            : null;
    const sessionDisplayId = desiredDisplayId ?? (canReuseTaskSessionParams ? taskSessionDisplayId : null);
    if (!sessionDisplayId && !sessionParams)
        return null;
    return {
        sessionDisplayId,
        sessionParams,
    };
}
export function normalizeUsageTotals(usage) {
    if (!usage)
        return null;
    return {
        inputTokens: Math.max(0, Math.floor(asNumber(usage.inputTokens, 0))),
        cachedInputTokens: Math.max(0, Math.floor(asNumber(usage.cachedInputTokens, 0))),
        outputTokens: Math.max(0, Math.floor(asNumber(usage.outputTokens, 0))),
    };
}
export function readRawUsageTotals(usageJson) {
    const parsed = parseObject(usageJson);
    if (Object.keys(parsed).length === 0)
        return null;
    const inputTokens = Math.max(0, Math.floor(asNumber(parsed.rawInputTokens, asNumber(parsed.inputTokens, 0))));
    const cachedInputTokens = Math.max(0, Math.floor(asNumber(parsed.rawCachedInputTokens, asNumber(parsed.cachedInputTokens, 0))));
    const outputTokens = Math.max(0, Math.floor(asNumber(parsed.rawOutputTokens, asNumber(parsed.outputTokens, 0))));
    if (inputTokens <= 0 && cachedInputTokens <= 0 && outputTokens <= 0) {
        return null;
    }
    return {
        inputTokens,
        cachedInputTokens,
        outputTokens,
    };
}
export function deriveNormalizedUsageDelta(current, previous) {
    if (!current)
        return null;
    if (!previous)
        return { ...current };
    const inputTokens = current.inputTokens >= previous.inputTokens
        ? current.inputTokens - previous.inputTokens
        : current.inputTokens;
    const cachedInputTokens = current.cachedInputTokens >= previous.cachedInputTokens
        ? current.cachedInputTokens - previous.cachedInputTokens
        : current.cachedInputTokens;
    const outputTokens = current.outputTokens >= previous.outputTokens
        ? current.outputTokens - previous.outputTokens
        : current.outputTokens;
    return {
        inputTokens: Math.max(0, inputTokens),
        cachedInputTokens: Math.max(0, cachedInputTokens),
        outputTokens: Math.max(0, outputTokens),
    };
}
export function formatCount(value) {
    if (typeof value !== "number" || !Number.isFinite(value))
        return "0";
    return value.toLocaleString("en-US");
}
export function parseSessionCompactionPolicy(agent) {
    return resolveSessionCompactionPolicy(agent.agentRuntimeType, agent.runtimeConfig).policy;
}
export function resolveRuntimeSessionParamsForWorkspace(input) {
    const { orgId, agent, previousSessionParams, resolvedWorkspace } = input;
    const previousSessionId = readNonEmptyString(previousSessionParams?.sessionId);
    if (!previousSessionId) {
        return {
            sessionParams: previousSessionParams,
            warning: null,
        };
    }
    const canonicalAgentCwd = readNonEmptyString(resolvedWorkspace.cwd) ?? resolveDefaultAgentWorkspaceDir(orgId, agent);
    if (!canonicalAgentCwd) {
        return {
            sessionParams: previousSessionParams,
            warning: null,
        };
    }
    const previousCwd = readNonEmptyString(previousSessionParams?.cwd);
    if (previousCwd && path.resolve(previousCwd) === path.resolve(canonicalAgentCwd)) {
        return {
            sessionParams: previousSessionParams,
            warning: null,
        };
    }
    const previousWorkspaceId = readNonEmptyString(previousSessionParams?.workspaceId);
    const migratedSessionParams = {
        ...(previousSessionParams ?? {}),
        cwd: canonicalAgentCwd,
    };
    if (!previousWorkspaceId ||
        !resolvedWorkspace.workspaceId ||
        previousWorkspaceId === resolvedWorkspace.workspaceId) {
        if (resolvedWorkspace.workspaceId)
            migratedSessionParams.workspaceId = resolvedWorkspace.workspaceId;
        if (resolvedWorkspace.repoUrl)
            migratedSessionParams.repoUrl = resolvedWorkspace.repoUrl;
        if (resolvedWorkspace.repoRef)
            migratedSessionParams.repoRef = resolvedWorkspace.repoRef;
    }
    return {
        sessionParams: migratedSessionParams,
        warning: previousCwd
            ? `Agent directory "${canonicalAgentCwd}" is now the canonical run directory. ` +
                `Attempting to resume session "${previousSessionId}" that was previously saved in "${previousCwd}".`
            : `Agent directory "${canonicalAgentCwd}" is now the canonical run directory. ` +
                `Attempting to resume session "${previousSessionId}" with the canonical agent directory attached.`,
    };
}
export function parseIssueAssigneeAgentRuntimeOverrides(raw) {
    const parsed = parseObject(raw);
    const parsedAdapterConfig = parseObject(parsed.agentRuntimeConfig);
    const agentRuntimeConfig = Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
    const useProjectWorkspace = typeof parsed.useProjectWorkspace === "boolean"
        ? parsed.useProjectWorkspace
        : null;
    if (!agentRuntimeConfig && useProjectWorkspace === null)
        return null;
    return {
        agentRuntimeConfig,
        useProjectWorkspace,
    };
}
export function deriveTaskKey(contextSnapshot, payload) {
    return (readNonEmptyString(contextSnapshot?.taskKey) ??
        readNonEmptyString(contextSnapshot?.taskId) ??
        readNonEmptyString(contextSnapshot?.issueId) ??
        readNonEmptyString(payload?.taskKey) ??
        readNonEmptyString(payload?.taskId) ??
        readNonEmptyString(payload?.issueId) ??
        null);
}
export function shouldResetTaskSessionForWake(contextSnapshot) {
    if (contextSnapshot?.forceFreshSession === true)
        return true;
    const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason);
    if (wakeReason === "issue_assigned")
        return true;
    return false;
}
export function formatRuntimeWorkspaceWarningLog(warning) {
    return {
        stream: "stdout",
        chunk: `[rudder] ${warning}\n`,
    };
}
export function describeSessionResetReason(contextSnapshot) {
    if (contextSnapshot?.forceFreshSession === true)
        return "forceFreshSession was requested";
    const wakeReason = readNonEmptyString(contextSnapshot?.wakeReason);
    if (wakeReason === "issue_assigned")
        return "wake reason is issue_assigned";
    return null;
}
export function deriveCommentId(contextSnapshot, payload) {
    return (readNonEmptyString(contextSnapshot?.wakeCommentId) ??
        readNonEmptyString(contextSnapshot?.commentId) ??
        readNonEmptyString(payload?.commentId) ??
        null);
}
export function enrichWakeContextSnapshot(input) {
    const { contextSnapshot, reason, source, triggerDetail, payload } = input;
    const issueIdFromPayload = readNonEmptyString(payload?.["issueId"]);
    const commentIdFromPayload = readNonEmptyString(payload?.["commentId"]);
    const taskKey = deriveTaskKey(contextSnapshot, payload);
    const wakeCommentId = deriveCommentId(contextSnapshot, payload);
    if (!readNonEmptyString(contextSnapshot["wakeReason"]) && reason) {
        contextSnapshot.wakeReason = reason;
    }
    if (!readNonEmptyString(contextSnapshot["issueId"]) && issueIdFromPayload) {
        contextSnapshot.issueId = issueIdFromPayload;
    }
    if (!readNonEmptyString(contextSnapshot["taskId"]) && issueIdFromPayload) {
        contextSnapshot.taskId = issueIdFromPayload;
    }
    if (!readNonEmptyString(contextSnapshot["taskKey"]) && taskKey) {
        contextSnapshot.taskKey = taskKey;
    }
    if (!readNonEmptyString(contextSnapshot["commentId"]) && commentIdFromPayload) {
        contextSnapshot.commentId = commentIdFromPayload;
    }
    if (!readNonEmptyString(contextSnapshot["wakeCommentId"]) && wakeCommentId) {
        contextSnapshot.wakeCommentId = wakeCommentId;
    }
    if (!readNonEmptyString(contextSnapshot["wakeSource"]) && source) {
        contextSnapshot.wakeSource = source;
    }
    if (!readNonEmptyString(contextSnapshot["wakeTriggerDetail"]) && triggerDetail) {
        contextSnapshot.wakeTriggerDetail = triggerDetail;
    }
    return {
        contextSnapshot,
        issueIdFromPayload,
        commentIdFromPayload,
        taskKey,
        wakeCommentId,
    };
}
export function mergeCoalescedContextSnapshot(existingRaw, incoming) {
    const existing = parseObject(existingRaw);
    const merged = {
        ...existing,
        ...incoming,
    };
    const commentId = deriveCommentId(incoming, null);
    if (commentId) {
        merged.commentId = commentId;
        merged.wakeCommentId = commentId;
    }
    return merged;
}
export function issueCommentAuthorKind(comment) {
    if (comment.authorAgentId)
        return "agent";
    if (comment.authorUserId)
        return "user";
    return "system";
}
export function issueCommentAuthorLabel(comment) {
    if (comment.authorAgentId) {
        return comment.authorAgentName?.trim() || `Agent ${comment.authorAgentId.slice(0, 8)}`;
    }
    if (comment.authorUserId) {
        return comment.authorUserName?.trim() || `User ${comment.authorUserId.slice(0, 8)}`;
    }
    return "System";
}
function formatIssuePromptDate(value) {
    if (!value)
        return "";
    const date = value instanceof Date ? value : new Date(value);
    return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
}
function issuePrincipalLabel(input) {
    if (!input.id)
        return null;
    const fallback = input.kind === "agent" ? `Agent ${input.id.slice(0, 8)}` : `User ${input.id.slice(0, 8)}`;
    return `${input.name?.trim() || fallback} (${input.kind})`;
}
export function buildDeferredWakePayload(payload, contextSnapshot, issueId) {
    const deferredPayload = { ...(payload ?? {}) };
    if (issueId && !readNonEmptyString(deferredPayload.issueId)) {
        deferredPayload.issueId = issueId;
    }
    deferredPayload[DEFERRED_WAKE_CONTEXT_KEY] = contextSnapshot;
    return deferredPayload;
}
export function readDeferredWakeContext(payloadRaw) {
    const payload = parseObject(payloadRaw);
    return parseObject(payload[DEFERRED_WAKE_CONTEXT_KEY]);
}
export function readDeferredWakePayload(payloadRaw) {
    const payload = { ...parseObject(payloadRaw) };
    delete payload[DEFERRED_WAKE_CONTEXT_KEY];
    return payload;
}
export function deriveDeferredWakeTaskKey(payloadRaw) {
    const payload = readDeferredWakePayload(payloadRaw);
    const contextSnapshot = readDeferredWakeContext(payloadRaw);
    return deriveTaskKey(contextSnapshot, payload);
}
export async function hydrateWakeContextSnapshot(db, orgId, contextSnapshot) {
    const issueId = readNonEmptyString(contextSnapshot.issueId);
    const commentId = deriveCommentId(contextSnapshot, null);
    const issueContext = parseObject(contextSnapshot.issue);
    const commentContext = parseObject(contextSnapshot.comment);
    const needsIssueContext = !!issueId;
    const needsProjectId = !!issueId && !readNonEmptyString(contextSnapshot.projectId);
    const needsCommentContext = !!commentId &&
        (!readNonEmptyString(commentContext.id) ||
            !readNonEmptyString(commentContext.body) ||
            !readNonEmptyString(commentContext.authorKind) ||
            !readNonEmptyString(commentContext.authorLabel) ||
            !readNonEmptyString(commentContext.createdAt));
    if (!needsIssueContext && !needsProjectId && !needsCommentContext)
        return;
    if (issueId && (needsIssueContext || needsProjectId)) {
        const issueRow = await db
            .select({
            id: issues.id,
            title: issues.title,
            description: issues.description,
            status: issues.status,
            priority: issues.priority,
            projectId: issues.projectId,
            assigneeAgentId: issues.assigneeAgentId,
            assigneeUserId: issues.assigneeUserId,
            reviewerAgentId: issues.reviewerAgentId,
            reviewerUserId: issues.reviewerUserId,
            createdAt: issues.createdAt,
            updatedAt: issues.updatedAt,
        })
            .from(issues)
            .where(and(eq(issues.id, issueId), eq(issues.orgId, orgId)))
            .then((rows) => rows[0] ?? null);
        if (issueRow) {
            const agentIds = [
                issueRow.assigneeAgentId,
                issueRow.reviewerAgentId,
            ].filter((value) => Boolean(value));
            const userIds = [
                issueRow.assigneeUserId,
                issueRow.reviewerUserId,
            ].filter((value) => Boolean(value));
            const agentNames = new Map();
            const userNames = new Map();
            if (agentIds.length > 0) {
                const agentRows = await db
                    .select({ id: agents.id, name: agents.name })
                    .from(agents)
                    .where(and(eq(agents.orgId, orgId), inArray(agents.id, agentIds)));
                for (const agentRow of agentRows)
                    agentNames.set(agentRow.id, agentRow.name);
            }
            if (userIds.length > 0) {
                const userRows = await db
                    .select({ id: authUsers.id, name: authUsers.name })
                    .from(authUsers)
                    .where(inArray(authUsers.id, userIds));
                for (const userRow of userRows)
                    userNames.set(userRow.id, userRow.name);
            }
            const assigneeLabel = issuePrincipalLabel({
                kind: "agent",
                id: issueRow.assigneeAgentId,
                name: issueRow.assigneeAgentId ? agentNames.get(issueRow.assigneeAgentId) : null,
            }) ??
                issuePrincipalLabel({
                    kind: "user",
                    id: issueRow.assigneeUserId,
                    name: issueRow.assigneeUserId ? userNames.get(issueRow.assigneeUserId) : null,
                }) ??
                "none";
            const reviewerLabel = issuePrincipalLabel({
                kind: "agent",
                id: issueRow.reviewerAgentId,
                name: issueRow.reviewerAgentId ? agentNames.get(issueRow.reviewerAgentId) : null,
            }) ??
                issuePrincipalLabel({
                    kind: "user",
                    id: issueRow.reviewerUserId,
                    name: issueRow.reviewerUserId ? userNames.get(issueRow.reviewerUserId) : null,
                }) ??
                "none";
            contextSnapshot.issue = {
                ...issueContext,
                id: issueRow.id,
                title: issueRow.title,
                description: issueRow.description,
                status: issueRow.status,
                priority: issueRow.priority,
                assigneeAgentId: issueRow.assigneeAgentId,
                assigneeUserId: issueRow.assigneeUserId,
                assigneeLabel,
                reviewerAgentId: issueRow.reviewerAgentId,
                reviewerUserId: issueRow.reviewerUserId,
                reviewerLabel,
                createdAt: formatIssuePromptDate(issueRow.createdAt),
                updatedAt: formatIssuePromptDate(issueRow.updatedAt),
            };
            if (!readNonEmptyString(contextSnapshot.projectId) && issueRow.projectId) {
                contextSnapshot.projectId = issueRow.projectId;
            }
        }
    }
    if (commentId && needsCommentContext) {
        const commentConditions = [eq(issueComments.id, commentId), eq(issueComments.orgId, orgId)];
        if (issueId) {
            commentConditions.push(eq(issueComments.issueId, issueId));
        }
        const commentRow = await db
            .select({
            id: issueComments.id,
            body: issueComments.body,
            authorAgentId: issueComments.authorAgentId,
            authorUserId: issueComments.authorUserId,
            authorAgentName: agents.name,
            authorUserName: authUsers.name,
            createdAt: issueComments.createdAt,
        })
            .from(issueComments)
            .leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
            .leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
            .where(and(...commentConditions))
            .then((rows) => rows[0] ?? null);
        if (commentRow) {
            contextSnapshot.comment = {
                ...commentContext,
                id: readNonEmptyString(commentContext.id) ?? commentRow.id,
                body: readNonEmptyString(commentContext.body) ?? commentRow.body,
                authorAgentId: "authorAgentId" in commentContext ? commentContext.authorAgentId : commentRow.authorAgentId,
                authorUserId: "authorUserId" in commentContext ? commentContext.authorUserId : commentRow.authorUserId,
                authorKind: readNonEmptyString(commentContext.authorKind) ?? issueCommentAuthorKind(commentRow),
                authorLabel: readNonEmptyString(commentContext.authorLabel) ?? issueCommentAuthorLabel(commentRow),
                createdAt: readNonEmptyString(commentContext.createdAt) ?? commentRow.createdAt.toISOString(),
            };
        }
    }
}
export function firstNonEmptyLine(value) {
    if (typeof value !== "string")
        return null;
    const line = value
        .split("\n")
        .map((chunk) => chunk.trim())
        .find(Boolean);
    return line ?? null;
}
export function deriveRecoveryFailureKind(run) {
    return (readNonEmptyString(run.errorCode) ??
        (run.status === "timed_out" ? "timed_out" : null) ??
        run.status);
}
export function deriveRecoveryFailureSummary(run) {
    return (firstNonEmptyLine(run.error) ??
        firstNonEmptyLine(run.stderrExcerpt) ??
        firstNonEmptyLine(run.stdoutExcerpt) ??
        (run.status === "timed_out" ? "The run timed out before it completed." : null) ??
        "The previous run failed before it completed.");
}
export function mergeMissingRecoveryContextFields(target, source) {
    const keysToBackfill = [
        "issueId",
        "taskId",
        "taskKey",
        "projectId",
        "projectWorkspaceId",
        "commentId",
        "wakeCommentId",
        "issue",
        "comment",
        "source",
        "wakeSource",
        "wakeTriggerDetail",
    ];
    for (const key of keysToBackfill) {
        if (!(key in target) || target[key] === null || target[key] === undefined || target[key] === "") {
            const value = source[key];
            if (value !== null && value !== undefined && value !== "") {
                target[key] = value;
            }
        }
    }
}
export async function hydrateRecoveryBaseContextSnapshot(run, getRunById) {
    const mergedContext = { ...parseObject(run.contextSnapshot) };
    let ancestorRunId = readNonEmptyString(run.retryOfRunId);
    let depth = 0;
    while (ancestorRunId && depth < MAX_RECOVERY_CHAIN_DEPTH) {
        const ancestorRun = await getRunById(ancestorRunId);
        if (!ancestorRun)
            break;
        mergeMissingRecoveryContextFields(mergedContext, parseObject(ancestorRun.contextSnapshot));
        ancestorRunId = readNonEmptyString(ancestorRun.retryOfRunId);
        depth += 1;
    }
    return mergedContext;
}
export function buildRecoveryContextSnapshot(input) {
    const { baseContextSnapshot, run, recoveryTrigger, wakeReason, wakeSource, triggerDetail } = input;
    const failureKind = deriveRecoveryFailureKind(run);
    const failureSummary = deriveRecoveryFailureSummary(run);
    const recovery = {
        originalRunId: run.id,
        failureKind,
        failureSummary,
        recoveryTrigger,
        recoveryMode: "continue_preferred",
    };
    return {
        ...baseContextSnapshot,
        wakeReason,
        wakeSource,
        wakeTriggerDetail: triggerDetail,
        retryOfRunId: run.id,
        retryReason: failureKind,
        recovery,
    };
}
export function normalizePassiveFollowupContext(raw) {
    const parsed = parseObject(raw);
    const originRunId = readNonEmptyString(parsed.originRunId);
    if (!originRunId)
        return null;
    const attempt = Math.max(0, Math.floor(asNumber(parsed.attempt, 0)));
    return {
        originRunId,
        previousRunId: readNonEmptyString(parsed.previousRunId),
        attempt,
        maxAttempts: Math.max(1, Math.floor(asNumber(parsed.maxAttempts, ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS))),
        reason: ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON,
        queuedAt: readNonEmptyString(parsed.queuedAt),
    };
}
export function normalizeReviewCloseoutContext(raw) {
    const parsed = parseObject(raw);
    const originRunId = readNonEmptyString(parsed.originRunId);
    if (!originRunId)
        return null;
    const attempt = Math.max(0, Math.floor(asNumber(parsed.attempt, 0)));
    return {
        originRunId,
        previousRunId: readNonEmptyString(parsed.previousRunId),
        attempt,
        maxAttempts: Math.max(1, Math.floor(asNumber(parsed.maxAttempts, ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS))),
        reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
    };
}
export function passiveFollowupCooldownMs(attempt) {
    return ISSUE_PASSIVE_FOLLOWUP_COOLDOWN_MS_BY_ATTEMPT.get(attempt) ?? 5 * 60 * 1000;
}
export function issueHasReviewer(issue) {
    return Boolean(issue.reviewerAgentId || issue.reviewerUserId);
}
export function isAgentEligibleForTimerContinuation(agent) {
    return (agent.status !== "paused" &&
        agent.status !== "terminated" &&
        agent.status !== "pending_approval");
}
export function hasCredibleTimerContinuation(input) {
    if (!input.policy.enabled || input.policy.intervalSec <= 0)
        return false;
    if (!isAgentEligibleForTimerContinuation(input.agent))
        return false;
    const intervalMs = input.policy.intervalSec * 1000;
    const nearTermWindowMs = Math.min(intervalMs * 2, ISSUE_PASSIVE_FOLLOWUP_TIMER_CONTINUITY_MAX_WINDOW_MS);
    const lastHeartbeatMs = input.agent.lastHeartbeatAt
        ? new Date(input.agent.lastHeartbeatAt).getTime()
        : new Date(input.agent.createdAt).getTime();
    const runFinishedMs = input.run.finishedAt
        ? new Date(input.run.finishedAt).getTime()
        : input.now.getTime();
    const baselineMs = Math.max(lastHeartbeatMs, runFinishedMs);
    const nextTimerMs = baselineMs + intervalMs;
    return Math.max(0, nextTimerMs - input.now.getTime()) <= nearTermWindowMs;
}
export function buildPassiveFollowupContextSnapshot(input) {
    const baseContext = { ...parseObject(input.run.contextSnapshot) };
    delete baseContext.recovery;
    delete baseContext.retryOfRunId;
    delete baseContext.retryReason;
    const taskKey = deriveTaskKey(baseContext, { issueId: input.issue.id }) ?? input.issue.id;
    return {
        ...baseContext,
        issueId: input.issue.id,
        taskId: input.issue.id,
        taskKey,
        projectId: readNonEmptyString(baseContext.projectId) ?? input.issue.projectId ?? undefined,
        wakeReason: ISSUE_PASSIVE_FOLLOWUP_REASON,
        wakeSource: ISSUE_PASSIVE_FOLLOWUP_WAKE_SOURCE,
        wakeTriggerDetail: "system",
        issue: {
            id: input.issue.id,
            title: input.issue.title,
            description: input.issue.description,
            status: input.issue.status,
            priority: input.issue.priority,
        },
        passiveFollowup: {
            originRunId: input.originRunId,
            previousRunId: input.run.id,
            attempt: input.attempt,
            maxAttempts: ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS,
            reason: ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON,
            queuedAt: input.now.toISOString(),
        },
        ...(issueHasReviewer(input.issue)
            ? {
                reviewGate: {
                    reviewerAgentId: input.issue.reviewerAgentId,
                    reviewerUserId: input.issue.reviewerUserId,
                    closeOutRequirement: "Move the issue to in_review when work is ready, or to blocked/cancelled if it cannot proceed.",
                },
            }
            : {}),
    };
}
export function runTaskKey(run) {
    return deriveTaskKey(run.contextSnapshot, null);
}
export function isSameTaskScope(left, right) {
    return (left ?? null) === (right ?? null);
}
export function isTrackedLocalChildProcessAdapter(agentRuntimeType) {
    return SESSIONED_LOCAL_ADAPTERS.has(agentRuntimeType);
}
// A positive liveness check means some process currently owns the PID.
// On Linux, PIDs can be recycled, so this is a best-effort signal rather
// than proof that the original child is still alive.
export function isProcessAlive(pid) {
    if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        const code = error?.code;
        if (code === "EPERM")
            return true;
        if (code === "ESRCH")
            return false;
        return false;
    }
}
export async function waitForProcessExit(pid, timeoutMs) {
    const deadline = Date.now() + Math.max(0, timeoutMs);
    while (Date.now() < deadline) {
        if (!isProcessAlive(pid))
            return true;
        await new Promise((resolve) => setTimeout(resolve, ORPHANED_PROCESS_POLL_INTERVAL_MS));
    }
    return !isProcessAlive(pid);
}
export async function terminateOrphanedProcess(pid) {
    if (!isProcessAlive(pid)) {
        return {
            stillAlive: false,
            terminationSignal: null,
            error: null,
        };
    }
    let terminationSignal = null;
    try {
        process.kill(pid, "SIGTERM");
        terminationSignal = "SIGTERM";
    }
    catch (error) {
        const code = error?.code;
        if (code === "ESRCH") {
            return {
                stillAlive: false,
                terminationSignal: null,
                error: null,
            };
        }
        return {
            stillAlive: isProcessAlive(pid),
            terminationSignal,
            error: `SIGTERM failed: ${error instanceof Error ? error.message : String(error)}`,
        };
    }
    if (await waitForProcessExit(pid, ORPHANED_PROCESS_TERMINATION_GRACE_MS)) {
        return {
            stillAlive: false,
            terminationSignal,
            error: null,
        };
    }
    try {
        process.kill(pid, "SIGKILL");
        terminationSignal = "SIGKILL";
    }
    catch (error) {
        const code = error?.code;
        if (code === "ESRCH") {
            return {
                stillAlive: false,
                terminationSignal,
                error: null,
            };
        }
        return {
            stillAlive: isProcessAlive(pid),
            terminationSignal,
            error: `SIGKILL failed: ${error instanceof Error ? error.message : String(error)}`,
        };
    }
    const exitedAfterKill = await waitForProcessExit(pid, ORPHANED_PROCESS_KILL_WAIT_MS);
    return {
        stillAlive: !exitedAfterKill,
        terminationSignal,
        error: exitedAfterKill ? null : `Timed out waiting for child pid ${pid} to exit after ${terminationSignal}`,
    };
}
export function truncateDisplayId(value, max = 128) {
    if (!value)
        return null;
    return value.length > max ? value.slice(0, max) : value;
}
export function normalizeAgentNameKey(value) {
    if (typeof value !== "string")
        return null;
    const normalized = value.trim().toLowerCase();
    return normalized.length > 0 ? normalized : null;
}
export const defaultSessionCodec = {
    deserialize(raw) {
        const asObj = parseObject(raw);
        if (Object.keys(asObj).length > 0)
            return asObj;
        const sessionId = readNonEmptyString(raw?.sessionId);
        if (sessionId)
            return { sessionId };
        return null;
    },
    serialize(params) {
        if (!params || Object.keys(params).length === 0)
            return null;
        return params;
    },
    getDisplayId(params) {
        return readNonEmptyString(params?.sessionId);
    },
};
export function getAgentRuntimeSessionCodec(agentRuntimeType) {
    const adapter = getServerAdapter(agentRuntimeType);
    return adapter.sessionCodec ?? defaultSessionCodec;
}
export function normalizeSessionParams(params) {
    if (!params)
        return null;
    return Object.keys(params).length > 0 ? params : null;
}
export function resolveNextSessionState(input) {
    const { codec, adapterResult, previousParams, previousDisplayId, previousLegacySessionId } = input;
    if (adapterResult.clearSession) {
        return {
            params: null,
            displayId: null,
            legacySessionId: null,
        };
    }
    const explicitParams = adapterResult.sessionParams;
    const hasExplicitParams = adapterResult.sessionParams !== undefined;
    const hasExplicitSessionId = adapterResult.sessionId !== undefined;
    const explicitSessionId = readNonEmptyString(adapterResult.sessionId);
    const hasExplicitDisplay = adapterResult.sessionDisplayId !== undefined;
    const explicitDisplayId = readNonEmptyString(adapterResult.sessionDisplayId);
    const shouldUsePrevious = !hasExplicitParams && !hasExplicitSessionId && !hasExplicitDisplay;
    const candidateParams = hasExplicitParams
        ? explicitParams
        : hasExplicitSessionId
            ? (explicitSessionId ? { sessionId: explicitSessionId } : null)
            : previousParams;
    const serialized = normalizeSessionParams(codec.serialize(normalizeSessionParams(candidateParams) ?? null));
    const deserialized = normalizeSessionParams(codec.deserialize(serialized));
    const displayId = truncateDisplayId(explicitDisplayId ??
        (codec.getDisplayId ? codec.getDisplayId(deserialized) : null) ??
        readNonEmptyString(deserialized?.sessionId) ??
        (shouldUsePrevious ? previousDisplayId : null) ??
        explicitSessionId ??
        (shouldUsePrevious ? previousLegacySessionId : null));
    const legacySessionId = explicitSessionId ??
        readNonEmptyString(deserialized?.sessionId) ??
        displayId ??
        (shouldUsePrevious ? previousLegacySessionId : null);
    return {
        params: serialized,
        displayId,
        legacySessionId,
    };
}
//# sourceMappingURL=heartbeat.sessions.js.map