import type { ReleaseRecoveryBlockedNoticeKind } from "../domain/policy.js"; import type { InvokableAgentSnapshot, IssueSnapshot, PostCommitEffect, ReleaseOutcome, RunSnapshot, RunSummary, } from "./types.js"; export type { InvokableAgentSnapshot, IssueSnapshot, ReleaseRecoveryBlockedNoticeKind, RunSnapshot, RunSummary }; /** The primary issue a locked release resolves to, plus the finishing run the lock step already loaded. */ export type LockedIssueExecution = { primaryIssue: IssueSnapshot; run: RunSnapshot; }; export type ReleaseTransactionResult = { outcome: ReleaseOutcome; postCommitEffects: PostCommitEffect[]; }; /** * The three host callbacks the release use case needs. These members do * not run on the module's own transaction, which is why two of them take * the transaction-scoped issue snapshot instead of an issue id. */ export interface WakeQueueHost { /** * Takes the transaction-scoped issue snapshot, not an issue id, so this * port never re-reads the issue on a separate connection while the * module's own transaction is open. */ resolveResponsibleUserId(input: { companyId: string; contextSnapshot: Record; issue: IssueSnapshot; /** From a prior `getRoutineEnv` call against the same issue; pass `{ routineId: null, env: null, responsibleUserId: null }` when the issue is not a routine execution. */ routineEnvContext: { routineId: string | null; env: unknown; responsibleUserId: string | null }; requestedByActorType: "user" | "agent" | "system" | null; requestedByActorId: string | null; source: string; triggerDetail: string | null; existingRunResponsibleUserId: string | null; }): Promise; /** * Takes the transaction-scoped issue snapshot, not an issue id, so this * port never re-reads the issue on a separate connection while the * module's own transaction is open. */ getRoutineEnv(input: { companyId: string; issue: IssueSnapshot; }): Promise<{ routineId: string | null; env: unknown; responsibleUserId: string | null }>; resolveSessionBeforeForWakeup(input: { companyId: string; agentId: string; taskKey: string | null; }): Promise; } export type DeferredWakeCandidate = { id: string; companyId: string; agentId: string; reason: string | null; source: string | null; triggerDetail: string | null; requestedByActorType: "user" | "agent" | "system" | null; requestedByActorId: string | null; payload: Record; /** The queued comment ids the wake's queued-comment context carries, already extracted from the payload. */ queuedCommentIds: string[]; /** True when the wake carries an independent reason to continue even with no live queued comments. */ preservesIndependentContinuation: boolean; /** `payload._paperclipWakeContext`, already parsed to a plain object. */ deferredContextSeed: Record; /** The comment ids the wake's context snapshot carries (a separate set from queuedCommentIds), used for the reopen check. */ deferredCommentIds: string[]; wakeReason: string | null; }; export type PromoteDeferredWakeInput = { companyId: string; wakeId: string; deferredAgent: InvokableAgentSnapshot; issue: IssueSnapshot; finishingRun: RunSnapshot; contextSnapshot: Record; reason: string; source: string; triggerDetail: string | null; payload: Record; responsibleUserId: string; sessionBefore: string | null; now: Date; }; /** * Every member is bound to the one transaction that `withIssueExecutionLock` * owns. The interface holds both reads and writes that drain and resolve * the deferred-wake queue. */ export interface WakeQueueTransaction { findInvokableAgent(input: { companyId: string; agentId: string }): Promise; findNextDeferredWake(input: { companyId: string; issueId: string }): Promise; getQueuedCommentLiveness(input: { companyId: string; issueId: string; wakeAgentId: string; finishingRunId: string; finishingRunAgentId: string; queuedCommentIds: string[]; }): Promise<{ liveNonSelfCommentIds: string[]; containedSelfAuthoredComment: boolean }>; /** Cancels the wake with `status = 'deferred_issue_execution'` as an atomic compare-and-set guard. */ cancelDeferredWake(input: { companyId: string; wakeId: string; reason: string; now: Date; }): Promise; normalizeDeferredWakeCommentIds(input: { companyId: string; wakeId: string; /** The wake's current payload, as already read by `findNextDeferredWake`, used as the rewrite base. */ payload: Record; liveCommentIds: string[]; now: Date; }): Promise; /** Sets `status = 'failed'` guarded by the current `deferred_issue_execution` status. */ failDeferredWake(input: { companyId: string; wakeId: string; now: Date }): Promise; getPauseHoldFacts(input: { companyId: string; issueId: string; wakeAgentId: string; deferredContextSeed: Record; requestedByActorType: string | null; requestedByActorId: string | null; }): Promise<{ activePauseHold: boolean; treeHoldInteractionWake: boolean; holdId: string | null; rootIssueId: string | null; mode: string | null; reason: string | null; releasePolicy: unknown; }>; getCommentSelfAuthorship(input: { companyId: string; issueId: string; finishingRunId: string; commentIds: string[]; }): Promise<{ allSelfAuthored: boolean }>; reopenIssue(input: { companyId: string; issueId: string; runId: string }): Promise; /** * Atomically claims the wake for promotion, guarded on its current * `deferred_issue_execution` status. Call this before any other write in * the promotion path (including a reopen), so a lost race here can never * leave another write committed underneath it. Returns `false` when a * concurrent writer already changed the wake's status. */ claimDeferredWakeForPromotion(input: { companyId: string; wakeId: string; now: Date }): Promise; /** * Finalizes a wake that `claimDeferredWakeForPromotion` already claimed: * inserts the queued run, links it back onto the wake row, and takes the * issue's execution lock. Call only after that claim returns `true`. */ finalizePromotedWake(input: PromoteDeferredWakeInput): Promise; /** An open run already on this issue (optionally scoped to one agent) that would race a new recovery run. */ hasExistingExecutionPath(input: { companyId: string; issueId: string; excludeRunId: string; agentId: string | null; }): Promise; /** An open, non-hidden issue that still lists this issue as a `blocks` predecessor. */ hasExplicitBlockerPath(input: { companyId: string; issueId: string }): Promise; isAutomaticRecoverySuppressedByPauseHold(input: { companyId: string; issueId: string }): Promise; queueReviewParticipantRecoveryRun(input: { companyId: string; issue: IssueSnapshot; finishingRun: RunSnapshot; recoveryAgent: InvokableAgentSnapshot; sessionBefore: string | null; now: Date; }): Promise; /** * Queues the run with the context snapshot and the responsible user the * caller already resolved. */ queueImmediateRecoveryRun(input: { companyId: string; issue: IssueSnapshot; finishingRun: RunSnapshot; recoveryAgent: InvokableAgentSnapshot; /** The wakeup request's reason and the run's context-snapshot wakeReason; the caller derives it from the issue status. */ reason: string; contextSnapshot: Record; responsibleUserId: string; sessionBefore: string | null; now: Date; }): Promise; } /** * Owns the module's own transaction: loads the finishing run, locks the * context issue and every sibling issue in id order, clears the two * release-lock columns, and picks the primary issue. When the primary * issue is missing, already reclaimed, or resolved by an early exit * (workspace-validation block, legacy reconciliation, a native-runtime * terminal failure), the adapter returns that outcome directly without * calling `fn`. Otherwise it calls `fn` with the locked issue and run, and * with `host`/`transaction` ports, so every call `fn` makes through the * transaction port participates in the one transaction this method owns. */ export interface IssueLockWriter { withIssueExecutionLock( input: { companyId: string; runId: string; now: Date }, fn: ( locked: LockedIssueExecution, ports: { host: WakeQueueHost; transaction: WakeQueueTransaction }, ) => Promise, ): Promise; } export type StrandedAssignedIssueEscalationInput = { issue: IssueSnapshot; previousStatus: "todo" | "in_progress" | "in_review"; latestRun: RunSnapshot; noticeKind: ReleaseRecoveryBlockedNoticeKind; }; export type StrandedRecoveryInPlaceEscalationInput = { issue: IssueSnapshot; previousStatus: "todo" | "in_progress" | "in_review"; latestRun: RunSnapshot; }; /** Wraps `services/recovery`'s stranded-issue escalation, called only after the release transaction commits. */ export interface RecoveryEscalationPort { escalateStrandedAssignedIssue(input: StrandedAssignedIssueEscalationInput): Promise; escalateStrandedRecoveryIssueInPlace(input: StrandedRecoveryInPlaceEscalationInput): Promise; } export type { PostCommitEffect, ReleaseOutcome };