mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/12/changes/paperclip/12 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Its external-agent module owns Muse assignments, operation receipts, and worker holds. > - Running assignments and retained terminal history must not cause an empty maintenance scan every five seconds. > - Terminal run changes and operation results already have durable database transactions. > - This pull request wakes the existing maintenance worker from those commits and reconstructs work at startup. > - The worker retries only concrete settlement debt. It retains qualification deadlines and authority checks. ## Linked Issues or Issue Description Refs: #17772. This is slice 57. Parent #17772 has merged. Related: #17000, #17525, #17752, #17758. The publication targets master and preserves the merged parent. **What existing behavior does this improve?** Muse terminal assignment settlement and the existing Muse maintenance worker. **Current behavior** A running assignment can keep maintenance scanning every five seconds. Terminal reconciliation rewrites unchanged history. Native finalization also calls that reconciliation during periodic replay. Discovery excludes a revoked original binding even when its assignment still needs settlement. **Proposed behavior** Committed run changes and operation receipts wake the existing worker with the original company, run or binding identity. Startup reconstructs actionable work from existing rows. Maintenance processes bounded batches and disarms after settlement. Default live and manual callbacks remain. The periodic callback delegates this one duty. **Reason and benefit** Configured but idle instances stop querying for this obligation. Late receipts, restarts, drain and historical bindings still have recovery paths. No table or independent timer is added. **Breaking changes** No schema, endpoint or provider protocol changes. Explicit manual reconciliation retains its unbounded default. This does not alter Cloud sleep admission or claim that all instance work can sleep. ## What Changed - Add an owner predicate shared by discovery and locked settlement. Fenced private-worker uncertainty remains held without an automatic retry loop. - Preserve original binding generation and company/agent/run identity, including revoked bindings. Fence only the terminal assignment during reconciliation. - Extend existing maintenance with scoped hints, bounded batches, concrete-debt retry, startup reconstruction and quiet drain behavior. - Register notifications inside existing terminal, projection, cancellation, operation-result and send-failure transactions. Keep qualification and OAuth signals explicitly global when coalesced with scoped changes. - Serialize only four domain IDs. Real native-finalizer tests caught and prevent a circular-object serialization failure at this boundary. - Use existing restore/worktree suppression and settings reactivation. Delegate both periodic finalizer branches while retaining resource callbacks and default live/manual settlement. - Add the producer audit and regression evidence in `doc/plans/2026-10-26-muse-terminal-settlement-sleep.md`. ## Verification Accepted production source: `584015018ed4a9d57d2b0f192b75847503b54e53`. Publication head: `305014b030d6c5eb891c70bc57036f4192d4fdad`. Normal adoption of the actual parent squash preserves the complete accepted child tree. Only audit text, a migration-count clarification, and the proved Photon test extension follow. - **3,715 distinct cases in 52 complete server files pass**, plus **20 selected chat cases** (3,735 selected total). The main 49-file batch passes 3,008; process recovery passes 410; review response passes 273; the full Photon file passes 24. The other 1,794 chat cases were not run locally. - **Quiet producer proof:** moving only the Muse fanout outside its parent guard produces six candidate SELECTs and fails the regression. Keeping the guard yields zero and still settles a real terminal run. The same test hash spans the controlled red/green pair. - **Photon review evidence:** repeated active callbacks produce zero settlement hints/candidate SQL. Three concurrent attention recoveries produce exactly one of each. The existing locked attention guard was correct; production needed no fix. The parent security check accepted the resolved finding before merge. This PR keeps the additional settlement queue in the existing regression. - **Static checks pass:** database build; server typecheck/build; module boundaries; migration order; current Copilot identity. Packages equal the accepted parent byte for byte. No new live provider qualification is claimed. - **Fixtures and failures:** 34 fresh PostgreSQL 17 fixtures have 412 applied migration receipts through tag 0413_tired_kronos, plus fresh per-call helper fixtures. All 52 original test hashes restore exactly. The additional Photon test hash matches its passing proof. Earlier remote chat passive-idle failure remains recorded; both passive/missing cases pass in the new selection. Parent handoff timeout/fresh-file-pass accounting remains in its audit. No assertion or timeout was weakened. - [Integration and quiet producer audit](https://github.com/paperclipai/paperclip/blob/305014b030d6c5eb891c70bc57036f4192d4fdad/doc/plans/2026-10-26-muse-terminal-settlement-sleep.md#current-master-integration-and-quiet-producers-2026-10-29). Fresh CI, security, and exact-head Greptile 5/5 must pass before merge. <details><summary>Earlier revision verification and failure history</summary> Receiver failure fairness: a controlled 1,100 ms batch reproduced a 30-second delay for healthy due receivers. The worker now supplies its existing per-item deadline through `nextRunAtOnError`. All 20 receiver lifecycle tests pass on the owner and combined tip `31c12f1d68ff4a51e06fc2be2abf2c2aee9f9d3a`; server production typechecking passes on the owner. The failed receiver retains its own backoff, and settled receivers return to zero recurring SQL. The five-second assertion budget is unchanged. See [the owning audit](https://github.com/paperclipai/paperclip/blob/f7477933469fdc960f555425dce66eff25a94e54/doc/plans/2026-10-25-chat-receiver-discovery-sleep-audit.md#healthy-receiver-progress-after-a-slow-failed-batch). Current-head CI and review remain required. Approved delegated Test-tab calls now evaluate caller and target policies in the existing claim transaction. Real HTTP regressions first showed a missing caller charge and shared-limit self-rejection. All 78 connection-management cases and 92 gateway cases now pass on the owning branch, along with server production typechecking. All 78 connection-management cases also pass at combined tip `d25f830db89c1623bff3547bfc68015c1c66e0cf`. The three new cases cover shared charges, separate charges, rollback on denial, duplicate approval, and rejection of a later authorized call. Intermediate counter/identity fixture failures and a reused-database Slack collision are recorded in [the owning audit](https://github.com/paperclipai/paperclip/blob/bbd31dea5ef3a4875950a87b4636af8b6bde858e/doc/idle-approved-tool-recovery.md#delegated-test-tab-admission-after-policy-changes). The earlier combined checks below remain evidence for their stated revisions. Current-head CI and review remain required. Inherited voice-cleanup verification: the controlled slow-provider regression and all 13 lifecycle tests passed on its owner and combined tip `ab22d8ed0c09028315607414b83b375e5cc0a60a`. That repair changed only the fixture clock advancement and its audit note. Current head: `f7477933469fdc960f555425dce66eff25a94e54`. This remains a single-duty PR stacked on #17772. - Integrated pinned master `8548173fc9efdc0fa07644fb92dcc0b6851a95e5` with normal merges. Every branch passed generated-schema checks and its target-branch migration-order gate. Existing SQL is unchanged; the 25 stack migrations follow master’s new migration. - Duty-specific producer coverage, lifecycle tests, and earlier failure records: [2026-10-24-recurring-server-sleep-stack.md](https://github.com/paperclipai/paperclip/blob/ff196791db6dada2dc04ea9db0d58c5c3beb4a41/doc/plans/2026-10-24-recurring-server-sleep-stack.md), [2026-10-26-muse-terminal-settlement-sleep.md](https://github.com/paperclipai/paperclip/blob/ff196791db6dada2dc04ea9db0d58c5c3beb4a41/doc/plans/2026-10-26-muse-terminal-settlement-sleep.md). - Combined tip `51e6cbe67979b55b340ea9f0f434c5b5c682ccd6`: **355 startup/cancellation/recovery/dispatch/preservation tests**, **75 connection-management tests**, and production server typechecking pass. These combined checks do not mean every intermediate PR independently ran the full repository suite. - Real master-to-stack database upgrade passes: **379 unchanged prior migration receipts plus 25 new migrations**, with the gateway responsibility fixture preserved. - Owning-branch checks: 84 runner installation/settlement cases plus runner typechecking and generated TypeScript/Rust profile agreement; 253 GitHub review workflow cases. Stop-attribution and held-worker readiness regressions failed before their fixes. - Failure accounting: the initial combined run passed 349 cases and failed six idle assertions in a reused preservation database containing old cancelled runs. Repeating the combined command with a fresh preservation database passed. Earlier invalid project selection, merge import/type errors, and the interrupted full local suite are not counted as passing checks. - **Latest-head CI and review remain required.** Prior CI findings include GitHub idle observation, route teardown, attention cleanup, touch-picker E2E, workspace health, and voice-cleanup timing; local passes do not erase those failures. No full local repository-suite, deployed sleep, fleet cost, or live Copilot qualification claim is made. </details> ## Risks - In-process signals do not capture independent SQL or other replicas. Supported external events still need ingress. - Pending assignments and operation receipts retain their original binding identity. A new agent configuration cannot take their ownership. - Genuine qualification, OAuth, and recovery deadlines remain obligations. They must not be discarded merely to make the worker quiet. - The parent coarse-hint guard prevents recurring Muse discovery; real company/binding authority changes retain their signals. - No full local repository suite, full chat suite, live Muse or Copilot qualification, deployed sleep, database suspension, external alarm coverage, or cost savings is claimed. Existing Cloud blockers remain. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, code execution, real PostgreSQL tests, and GitHub tools. The exact runtime model identifier and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Codex <noreply@openai.com>
2454 lines
91 KiB
TypeScript
2454 lines
91 KiB
TypeScript
import { notifyExternalAgentSettlement } from "../../modules/external-agents/index.js";
|
|
import { lockCurrentNativeWorkAssessmentEvidence } from "./work-assessments.js";
|
|
import { hasNativeWorkspaceFinalizationOwner } from "./native-workspace-finalization-recovery-state.js";
|
|
import { tryLockNativeWorkspaceFinalization } from "./native-workspace-finalization-ownership.js";
|
|
import { lockCurrentNativeCompletionContract } from "./completion-contracts.js";
|
|
import { isNativeRunnerOwnershipHeld } from "./native-runner-ownership.js";
|
|
import { notifyNativeFinalizationWork } from "../native-finalization-notifications.js";
|
|
import { notifyUnsafeWorkspaceExportRecovery } from "../unsafe-workspace-export-notifications.js";
|
|
import { notifyAutomaticCompletionReviews } from "../automatic-completion-review-notifications.js";
|
|
import { notifyObsoletePolicyReviews } from "../obsolete-policy-review-notifications.js";
|
|
import { notifyCancelledWorkspacePreservation } from "../cancelled-workspace-preservation-notifications.js";
|
|
import { notifyInstructionCollection } from "../instruction-collection-notifications.js";
|
|
import { notifyChatRunMilestones } from "../chat-run-milestone-notifications.js";
|
|
import { scheduledIssueMonitorWait } from "../issue-monitors.js";
|
|
import { onboardingFirstTaskCompletionAllowed } from "../onboarding-first-task-admission.js";
|
|
import { hasPublishedGitHubReview, readInheritedGitHubReviewWait } from "../chat-channels/github/review-wait.js";
|
|
import { lockNativeGovernanceSources, pendingNativeGovernance } from "./native-governance.js";
|
|
import { findSupersededExternalReviewRequests, supersedeExternalReviewRequestsInTransaction } from "./superseded-external-review-requests.js";
|
|
import { normalizePrpResultSignals, type PrpStructuredRunResult } from "../../vendor/paperclip-runner/index.js";
|
|
import { readNativeMonitorDelivery, readNativePreservedMonitorWait, type NativeMonitorDelivery, type NativePreservedMonitorWait } from "./native-monitor-delivery.js";
|
|
import { readNativePlanWait, type NativePlanWaitSource } from "./native-plan-wait.js";
|
|
import { activeIssueInteractionCondition } from "../issue-question-context.js";
|
|
import { isConversation } from "../agent-conversations.js";
|
|
import { hasPendingNativeChildCompletion, type NativeChildCompletionRecipient } from "./native-child-completion-delivery.js";
|
|
import { and, asc, desc, eq, inArray, notInArray, sql } from "drizzle-orm";
|
|
import type { Db } from "@paperclipai/db";
|
|
import type { CreateIssueThreadInteraction } from "@paperclipai/shared";
|
|
import {
|
|
agentWakeupRequests,
|
|
agents,
|
|
approvals,
|
|
completionContracts,
|
|
heartbeatRuns,
|
|
issueApprovals,
|
|
issueRecoveryActions,
|
|
issueThreadInteractions,
|
|
issues,
|
|
nativeRunFinalizations,
|
|
nativeRunResults,
|
|
statusDecisionEffects,
|
|
statusDecisions,
|
|
workAssessments,
|
|
workspaceOperations,
|
|
} from "@paperclipai/db";
|
|
import {
|
|
NATIVE_STATUS_ARBITER_POLICY_VERSION,
|
|
type NativeGovernanceGate,
|
|
type NativeStatusDecision,
|
|
type NativeStatusEffect,
|
|
} from "./status-arbiter.js";
|
|
import { nativeSha256 } from "./canonical.js";
|
|
import { getNativeReviewAssignment, type NativeReviewAssignmentContext } from "./native-review-participant.js";
|
|
import {
|
|
readNativeBoardResponseWaitSource,
|
|
readNativeBoardResponseWaitOrigin,
|
|
type NativeBoardResponseWaitOrigin,
|
|
type NativeBoardResponseWaitSource,
|
|
} from "./native-board-response-wait.js";
|
|
import {
|
|
prepareNativeChatReviewPresentationInTransaction,
|
|
restoreNativeChatReviewPresentationInTransaction,
|
|
} from "./native-chat-review-presentation.js";
|
|
import {
|
|
readPublishedGitHubReviewCompletionContinuationInTransaction,
|
|
isExternalChatWaitAuthorizationContention,
|
|
resolveExternalChatResponseWaitAuthorizationInTransaction,
|
|
} from "./chat-attachment-reuse.js";
|
|
import { issueService } from "../issues.js";
|
|
import { syncRoutineRunStatusForIssue } from "../routine-run-status.js";
|
|
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
|
|
import { issueRecoveryActionService } from "../issue-recovery-actions.js";
|
|
import { buildIssueBlockersResolvedWakeIdempotencyKey } from "../issue-dependency-wakeups.js";
|
|
import {
|
|
persistActivity,
|
|
publishActivity,
|
|
type ActivityPublication,
|
|
} from "../activity-log.js";
|
|
import { emitAgentTaskRun } from "../agent-task-run-telemetry.js";
|
|
import { materializeNativeProviderCapacityRetry } from "./native-provider-capacity-retry.js";
|
|
import { NATIVE_PROVIDER_OVERLOADED_CODE } from "./native-provider-failure.js";
|
|
import { DELIVERY_QUEUES, notifyDeliveryWork } from "../delivery-work-notifications.js";
|
|
|
|
export class NativeStatusRaceError extends Error {
|
|
readonly code = "native_status_race" as const;
|
|
constructor() {
|
|
super("native_status_race");
|
|
this.name = "NativeStatusRaceError";
|
|
}
|
|
}
|
|
|
|
export function nativePendingEffectDueAt(coordinator: {
|
|
phase: string; resultId: string | null; nextAttemptAt: Date | null;
|
|
leaseOwner: string | null; leaseExpiresAt: Date | null;
|
|
}): number | null {
|
|
if (coordinator.phase === "terminal_failure" || coordinator.resultId === null) return null;
|
|
return Math.max(coordinator.nextAttemptAt?.getTime() ?? 0,
|
|
coordinator.leaseOwner !== null ? coordinator.leaseExpiresAt?.getTime() ?? 0 : 0);
|
|
}
|
|
|
|
/** Reconciliation delivery resumes ledger work only; it never re-arbitrates. */
|
|
export async function dispatchPendingNativeStatusEffects(input: {
|
|
db: Db;
|
|
companyId: string;
|
|
issueId: string;
|
|
decisionId: string;
|
|
runId: string;
|
|
onlyWhenDue?: boolean;
|
|
}) {
|
|
return input.db.transaction(async (tx) => {
|
|
const coordinator = await tx
|
|
.select({
|
|
decisionId: nativeRunFinalizations.decisionId,
|
|
phase: nativeRunFinalizations.phase,
|
|
resultId: nativeRunFinalizations.resultId,
|
|
nextAttemptAt: nativeRunFinalizations.nextAttemptAt,
|
|
leaseOwner: nativeRunFinalizations.leaseOwner,
|
|
leaseExpiresAt: nativeRunFinalizations.leaseExpiresAt,
|
|
})
|
|
.from(nativeRunFinalizations)
|
|
.where(
|
|
and(
|
|
eq(nativeRunFinalizations.runId, input.runId),
|
|
eq(nativeRunFinalizations.companyId, input.companyId),
|
|
eq(nativeRunFinalizations.issueId, input.issueId),
|
|
),
|
|
)
|
|
.for("update")
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!coordinator || coordinator.decisionId !== input.decisionId) {
|
|
throw new Error("native_pending_effect_decision_binding_mismatch");
|
|
}
|
|
if (input.onlyWhenDue) {
|
|
const dueAt = nativePendingEffectDueAt(coordinator);
|
|
if (dueAt === null || dueAt > Date.now()) return [];
|
|
}
|
|
const pending = await tx
|
|
.select({
|
|
id: statusDecisionEffects.id,
|
|
effectKind: statusDecisionEffects.effectKind,
|
|
targetType: statusDecisionEffects.targetType,
|
|
targetId: statusDecisionEffects.targetId,
|
|
})
|
|
.from(statusDecisionEffects)
|
|
.where(
|
|
and(
|
|
eq(statusDecisionEffects.companyId, input.companyId),
|
|
eq(statusDecisionEffects.issueId, input.issueId),
|
|
eq(statusDecisionEffects.decisionId, input.decisionId),
|
|
inArray(statusDecisionEffects.deliveryState, ["pending", "failed"]),
|
|
),
|
|
)
|
|
.for("update");
|
|
if (pending.length === 0) return [];
|
|
for (const effect of pending) {
|
|
await assertPendingEffectTarget(tx as unknown as Db, {
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
effectKind: effect.effectKind,
|
|
targetType: effect.targetType,
|
|
targetId: effect.targetId,
|
|
});
|
|
}
|
|
await notifyNativeFinalizationWork(tx, input);
|
|
await notifyChatRunMilestones(tx, input);
|
|
await tx
|
|
.update(statusDecisionEffects)
|
|
.set({
|
|
deliveryState: "delivered",
|
|
attemptCount: sql`${statusDecisionEffects.attemptCount} + 1`,
|
|
deliveredAt: new Date(),
|
|
nextAttemptAt: null,
|
|
lastError: null,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
inArray(
|
|
statusDecisionEffects.id,
|
|
pending.map((row) => row.id),
|
|
),
|
|
);
|
|
await updateRunEffectState({
|
|
tx: tx as unknown as Db,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeDispatchedEffectIds",
|
|
value: pending.map((row) => row.id),
|
|
});
|
|
return pending.map((row) => row.id);
|
|
});
|
|
}
|
|
|
|
export type NativeStatusCommitFailpoint =
|
|
| "governance_materialization"
|
|
| "interaction_materialization"
|
|
| "continuation_materialization"
|
|
| "blocker_materialization"
|
|
| "recovery_materialization"
|
|
| "status_projection";
|
|
|
|
export type NativeMaterializedStatusEffect = {
|
|
effectKind: string;
|
|
targetType: string;
|
|
targetId: string | null;
|
|
payload: Record<string, unknown>;
|
|
};
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
function assertNeverEffect(effect: never): never {
|
|
const kind = record(effect).kind;
|
|
throw new Error(
|
|
`native_status_effect_unimplemented:${String(kind ?? "unknown")}`,
|
|
);
|
|
}
|
|
|
|
async function assertPendingEffectTarget(
|
|
tx: Db,
|
|
input: {
|
|
companyId: string;
|
|
issueId: string;
|
|
effectKind: string;
|
|
targetType: string;
|
|
targetId: string | null;
|
|
},
|
|
) {
|
|
if (!input.targetId)
|
|
throw new Error(`native_pending_effect_target_missing:${input.effectKind}`);
|
|
let found = false;
|
|
if (input.targetType === "agent_wakeup_request") {
|
|
found = await tx
|
|
.select({ id: agentWakeupRequests.id })
|
|
.from(agentWakeupRequests)
|
|
.where(
|
|
and(
|
|
eq(agentWakeupRequests.id, input.targetId),
|
|
eq(agentWakeupRequests.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "agent") {
|
|
found = await tx
|
|
.select({ id: agents.id })
|
|
.from(agents)
|
|
.where(
|
|
and(
|
|
eq(agents.id, input.targetId),
|
|
eq(agents.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (
|
|
input.targetType === "issue_thread_interaction" ||
|
|
input.targetType === "interaction"
|
|
) {
|
|
found = await tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.id, input.targetId),
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (
|
|
[
|
|
"delegated_issue",
|
|
"issue",
|
|
"issue_checkout",
|
|
"issue_unblock_descriptor",
|
|
].includes(input.targetType)
|
|
) {
|
|
found = await tx
|
|
.select({ id: issues.id })
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.targetId),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "issue_recovery_action") {
|
|
found = await tx
|
|
.select({ id: issueRecoveryActions.id })
|
|
.from(issueRecoveryActions)
|
|
.where(
|
|
and(
|
|
eq(issueRecoveryActions.id, input.targetId),
|
|
eq(issueRecoveryActions.companyId, input.companyId),
|
|
eq(issueRecoveryActions.sourceIssueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "heartbeat_run") {
|
|
found = await tx
|
|
.select({ id: heartbeatRuns.id })
|
|
.from(heartbeatRuns)
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.targetId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "workspace_operation") {
|
|
found = await tx
|
|
.select({ id: workspaceOperations.id })
|
|
.from(workspaceOperations)
|
|
.where(
|
|
and(
|
|
eq(workspaceOperations.id, input.targetId),
|
|
eq(workspaceOperations.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "completion_contract") {
|
|
found = await tx
|
|
.select({ id: completionContracts.id })
|
|
.from(completionContracts)
|
|
.where(
|
|
and(
|
|
eq(completionContracts.id, input.targetId),
|
|
eq(completionContracts.companyId, input.companyId),
|
|
eq(completionContracts.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "status_decision") {
|
|
found = await tx
|
|
.select({ id: statusDecisions.id })
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.id, input.targetId),
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
eq(statusDecisions.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "status_decision_effect") {
|
|
found = await tx
|
|
.select({ id: statusDecisionEffects.id })
|
|
.from(statusDecisionEffects)
|
|
.where(
|
|
and(
|
|
eq(statusDecisionEffects.id, input.targetId),
|
|
eq(statusDecisionEffects.companyId, input.companyId),
|
|
eq(statusDecisionEffects.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "approval") {
|
|
found = await tx
|
|
.select({ id: approvals.id })
|
|
.from(issueApprovals)
|
|
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
|
|
.where(
|
|
and(
|
|
eq(approvals.id, input.targetId),
|
|
eq(approvals.companyId, input.companyId),
|
|
eq(issueApprovals.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows.length === 1);
|
|
} else if (input.targetType === "execution_stage") {
|
|
found = await tx
|
|
.select({ executionState: issues.executionState })
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.issueId),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => record(rows[0]?.executionState).status === "pending");
|
|
} else {
|
|
throw new Error(
|
|
`native_pending_effect_target_unimplemented:${input.targetType}`,
|
|
);
|
|
}
|
|
if (!found)
|
|
throw new Error(`native_pending_effect_target_missing:${input.effectKind}`);
|
|
}
|
|
|
|
async function updateRunEffectState(input: {
|
|
tx: Db;
|
|
companyId: string;
|
|
runId: string;
|
|
key: string;
|
|
value: unknown;
|
|
runnerProfile?: boolean;
|
|
}) {
|
|
const run = await input.tx
|
|
.select({
|
|
id: heartbeatRuns.id,
|
|
resultJson: heartbeatRuns.resultJson,
|
|
runnerProfileJson: heartbeatRuns.runnerProfileJson,
|
|
})
|
|
.from(heartbeatRuns)
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.for("update")
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!run) throw new Error("native_status_run_missing");
|
|
const target = input.runnerProfile
|
|
? record(run.runnerProfileJson)
|
|
: record(run.resultJson);
|
|
await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
...(input.runnerProfile
|
|
? { runnerProfileJson: { ...target, [input.key]: input.value } }
|
|
: { resultJson: { ...target, [input.key]: input.value } }),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(heartbeatRuns.id, input.runId));
|
|
return run.id;
|
|
}
|
|
|
|
function failAt(
|
|
actual: NativeStatusCommitFailpoint,
|
|
requested?: NativeStatusCommitFailpoint,
|
|
) {
|
|
if (actual === requested)
|
|
throw new Error(`native_status_failpoint:${actual}`);
|
|
}
|
|
|
|
async function enqueueWake(input: {
|
|
tx: Db;
|
|
companyId: string;
|
|
issueId: string;
|
|
agentId: string;
|
|
reason: string;
|
|
idempotencyKey: string;
|
|
payload: Record<string, unknown>;
|
|
contextSnapshot?: Record<string, unknown>;
|
|
}) {
|
|
const existing = await input.tx
|
|
.select({ id: agentWakeupRequests.id })
|
|
.from(agentWakeupRequests)
|
|
.where(
|
|
and(
|
|
eq(agentWakeupRequests.companyId, input.companyId),
|
|
eq(agentWakeupRequests.idempotencyKey, input.idempotencyKey),
|
|
inArray(agentWakeupRequests.status, [
|
|
"queued",
|
|
"deferred_issue_execution",
|
|
"claimed",
|
|
"completed",
|
|
]),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (existing) return existing.id;
|
|
const inserted = await input.tx
|
|
.insert(agentWakeupRequests)
|
|
.values({
|
|
companyId: input.companyId,
|
|
agentId: input.agentId,
|
|
source: "automation",
|
|
triggerDetail: "system",
|
|
reason: input.reason,
|
|
payload: {
|
|
issueId: input.issueId,
|
|
taskId: input.issueId,
|
|
...input.payload,
|
|
_paperclipWakeContext: {
|
|
issueId: input.issueId,
|
|
taskId: input.issueId,
|
|
...(input.contextSnapshot ?? {}),
|
|
wakeReason: input.reason,
|
|
source: "native_status_decision",
|
|
statusDecisionSource: "native_status_decision",
|
|
},
|
|
},
|
|
requestedByActorType: "system",
|
|
requestedByActorId: "native-status-committer",
|
|
idempotencyKey: input.idempotencyKey,
|
|
})
|
|
.returning({ id: agentWakeupRequests.id })
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!inserted) throw new Error("native_status_wake_not_persisted");
|
|
return inserted.id;
|
|
}
|
|
|
|
async function validateGovernanceGate(
|
|
tx: Db,
|
|
input: {
|
|
companyId: string;
|
|
issueId: string;
|
|
gate: NonNullable<
|
|
Extract<NativeStatusEffect, { kind: "create_interaction" }>["gate"]
|
|
>;
|
|
executionState: unknown;
|
|
runId: string;
|
|
conversationMode: boolean;
|
|
},
|
|
) {
|
|
if (input.gate.kind === "execution_stage") {
|
|
const state =
|
|
input.executionState && typeof input.executionState === "object"
|
|
? (input.executionState as Record<string, unknown>)
|
|
: {};
|
|
if (state.status !== "pending")
|
|
throw new Error("native_governance_gate_resolved");
|
|
return input.gate.id;
|
|
}
|
|
if (input.gate.kind === "interaction") {
|
|
const interaction = await tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.id, input.gate.id),
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issueId),
|
|
eq(issueThreadInteractions.status, "pending"),
|
|
activeIssueInteractionCondition(input),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!interaction) throw new Error("native_governance_gate_resolved");
|
|
return interaction.id;
|
|
}
|
|
const approval = await tx
|
|
.select({ id: approvals.id })
|
|
.from(issueApprovals)
|
|
.innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id))
|
|
.where(
|
|
and(
|
|
eq(issueApprovals.companyId, input.companyId),
|
|
eq(issueApprovals.issueId, input.issueId),
|
|
eq(approvals.companyId, input.companyId),
|
|
eq(approvals.id, input.gate.id),
|
|
inArray(approvals.status, ["pending", "revision_requested"]),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!approval) throw new Error("native_governance_gate_resolved");
|
|
return approval.id;
|
|
}
|
|
|
|
async function materializeDecisionEffect(input: {
|
|
tx: Db;
|
|
companyId: string;
|
|
issue: typeof issues.$inferSelect;
|
|
runId: string;
|
|
decisionId: string;
|
|
effect: NativeStatusEffect;
|
|
failpoint?: NativeStatusCommitFailpoint;
|
|
preMaterializedEffects?: ReadonlyMap<string, NativeMaterializedStatusEffect>;
|
|
/** Terminal heartbeat_runs rows this effect wrote. The caller emits agent.task_run for each, after its transaction commits. */
|
|
terminalRunsToEmit?: (typeof heartbeatRuns.$inferSelect)[];
|
|
}): Promise<NativeMaterializedStatusEffect> {
|
|
const { effect } = input;
|
|
if (effect.kind === "create_interaction") {
|
|
failAt("governance_materialization", input.failpoint);
|
|
if (effect.gate) {
|
|
const targetId = await validateGovernanceGate(input.tx, {
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
gate: effect.gate,
|
|
executionState: input.issue.executionState,
|
|
runId: input.runId,
|
|
conversationMode: isConversation(input.issue),
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: effect.gate.kind,
|
|
targetId,
|
|
payload: { gate: effect.gate },
|
|
};
|
|
}
|
|
const interaction = await issueThreadInteractionService(input.tx).create(
|
|
input.issue,
|
|
{
|
|
kind: "ask_user_questions",
|
|
idempotencyKey: `native-attention:${input.decisionId}`,
|
|
sourceRunId: input.runId,
|
|
title: "Native attention request",
|
|
summary: "The native runner requires a board response.",
|
|
continuationPolicy: "wake_assignee",
|
|
payload: {
|
|
version: 1,
|
|
questions: [
|
|
{
|
|
id: "response",
|
|
prompt: effect.prompt ?? "Provide the required response.",
|
|
selectionMode: "single",
|
|
options: [{ id: "continue", label: "Continue" }],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{ systemId: "native-status-committer", runId: input.runId },
|
|
);
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_thread_interaction",
|
|
targetId: interaction.id,
|
|
payload: {
|
|
interactionId: interaction.id,
|
|
interactionKind: interaction.kind,
|
|
},
|
|
};
|
|
}
|
|
if (effect.kind === "bind_reviewer") {
|
|
failAt("interaction_materialization", input.failpoint);
|
|
const reviewInput: Extract<
|
|
CreateIssueThreadInteraction,
|
|
{ kind: "request_confirmation" }
|
|
> = {
|
|
kind: "request_confirmation",
|
|
idempotencyKey: `native-review:${input.decisionId}${effect.requestKey ? `:${effect.requestKey}` : ""}`,
|
|
sourceRunId: input.runId,
|
|
resolverPolicy: effect.ownerAgentId ? "anyone" : "human_only",
|
|
addresseeAgentId: effect.ownerAgentId ?? null,
|
|
addresseeUserId: effect.ownerUserId,
|
|
title: "Review requested",
|
|
summary:
|
|
effect.prompt,
|
|
continuationPolicy: "wake_assignee",
|
|
payload: {
|
|
version: 1,
|
|
prompt: effect.prompt,
|
|
detailsMarkdown: effect.detailsMarkdown ?? null,
|
|
acceptLabel: "Approve completion",
|
|
rejectLabel: "Continue work",
|
|
allowDeclineReason: true,
|
|
rejectRequiresReason: true,
|
|
supersedeOnUserComment: false,
|
|
target: {
|
|
type: "custom",
|
|
key: "native_completion_review",
|
|
revisionId: input.decisionId,
|
|
label: "Completion decision",
|
|
},
|
|
},
|
|
};
|
|
const interaction = await issueThreadInteractionService(input.tx).create(
|
|
input.issue,
|
|
reviewInput,
|
|
{ systemId: "native-status-committer", runId: input.runId },
|
|
);
|
|
// The card and its next actor commit together. The post-commit dispatcher
|
|
// revalidates this exact review before granting a scoped reviewer run.
|
|
const reviewContext = {
|
|
nativeReviewInteractionId: interaction.id,
|
|
nativeReviewDecisionId: input.decisionId,
|
|
};
|
|
const reviewerWakeId = effect.ownerAgentId && interaction.effectiveResolverPolicy !== "human_only"
|
|
? await enqueueWake({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
agentId: effect.ownerAgentId,
|
|
reason: "native_completion_review",
|
|
idempotencyKey: `native-review:${interaction.id}`,
|
|
payload: reviewContext,
|
|
contextSnapshot: { ...reviewContext, forceFreshSession: true },
|
|
})
|
|
: null;
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_thread_interaction",
|
|
targetId: interaction.id,
|
|
payload: {
|
|
interactionId: interaction.id,
|
|
interactionKind: interaction.kind,
|
|
ownerUserId: effect.ownerUserId,
|
|
ownerAgentId: effect.ownerAgentId ?? null,
|
|
reviewerWakeId,
|
|
},
|
|
};
|
|
}
|
|
if (effect.kind === "notify_owner") {
|
|
const wakeId = await enqueueWake({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
agentId: effect.agentId,
|
|
reason: "issue_status_changed",
|
|
idempotencyKey: `native-status:${input.decisionId}:notify-owner`,
|
|
payload: {
|
|
nativeDecisionId: input.decisionId,
|
|
notificationReason: effect.reason,
|
|
},
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "agent_wakeup_request",
|
|
targetId: wakeId,
|
|
payload: { reason: effect.reason },
|
|
};
|
|
}
|
|
if (effect.kind === "create_delegated_issue") {
|
|
const delegated = await issueService(input.tx).create(input.companyId, {
|
|
projectId: input.issue.projectId,
|
|
projectWorkspaceId: input.issue.projectWorkspaceId,
|
|
goalId: input.issue.goalId,
|
|
parentId: input.issue.id,
|
|
title: effect.summary,
|
|
status: "todo",
|
|
priority: input.issue.priority,
|
|
assigneeAgentId: effect.agentId,
|
|
createdByAgentId: input.issue.assigneeAgentId,
|
|
workMode: "standard",
|
|
idempotencyKey: `native-delegation:${input.decisionId}`,
|
|
inheritExecutionWorkspaceFromIssueId: input.issue.id,
|
|
});
|
|
if (!delegated) throw new Error("native_delegated_issue_not_persisted");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "delegated_issue",
|
|
targetId: delegated.id,
|
|
payload: { summary: effect.summary, agentId: effect.agentId },
|
|
};
|
|
}
|
|
if (effect.kind === "enqueue_continuation") {
|
|
failAt("continuation_materialization", input.failpoint);
|
|
if (
|
|
effect.idempotencyKey.startsWith(
|
|
`connection-intent:tools:${input.runId}:`,
|
|
)
|
|
) {
|
|
const [refresh] = await input.tx
|
|
.select({ id: agentWakeupRequests.id })
|
|
.from(agentWakeupRequests)
|
|
.where(
|
|
and(
|
|
eq(agentWakeupRequests.companyId, input.companyId),
|
|
eq(agentWakeupRequests.agentId, effect.agentId),
|
|
eq(agentWakeupRequests.idempotencyKey, effect.idempotencyKey),
|
|
notInArray(agentWakeupRequests.status, [
|
|
"skipped",
|
|
"failed",
|
|
"cancelled",
|
|
]),
|
|
),
|
|
)
|
|
.limit(1);
|
|
if (refresh)
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "agent_wakeup_request",
|
|
targetId: refresh.id,
|
|
payload: {
|
|
continuationKind: effect.continuationKind,
|
|
summary: effect.summary,
|
|
},
|
|
};
|
|
}
|
|
if (effect.continuationKind === "monitor") throw new Error("Monitor delivery belongs to the issue monitor scheduler");
|
|
const wakeId = await enqueueWake({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
agentId: effect.agentId,
|
|
reason: "issue_status_changed",
|
|
idempotencyKey: `native-status:${input.decisionId}:continuation`,
|
|
payload: {
|
|
nativeDecisionId: input.decisionId,
|
|
continuationKind: effect.continuationKind,
|
|
continuationSummary: effect.summary,
|
|
continuationIdempotencyKey: effect.idempotencyKey,
|
|
},
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "agent_wakeup_request",
|
|
targetId: wakeId,
|
|
payload: {
|
|
continuationKind: effect.continuationKind,
|
|
summary: effect.summary,
|
|
},
|
|
};
|
|
}
|
|
if (effect.kind === "bind_blocker") {
|
|
failAt("blocker_materialization", input.failpoint);
|
|
const [bound] = await input.tx
|
|
.update(issues)
|
|
.set({
|
|
unblockDescriptor: { owner: effect.owner, action: effect.action },
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.issue.id),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({ id: issues.id });
|
|
if (!bound) throw new Error("native_blocker_binding_not_persisted");
|
|
let wakeId: string | null = null;
|
|
if (effect.owner !== "board") {
|
|
wakeId = await enqueueWake({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
agentId: effect.owner.agentId,
|
|
reason: "issue_status_changed",
|
|
idempotencyKey: `native-status:${input.decisionId}:blocker-owner`,
|
|
payload: {
|
|
nativeDecisionId: input.decisionId,
|
|
unblockAction: effect.action,
|
|
},
|
|
});
|
|
}
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_unblock_descriptor",
|
|
targetId: bound.id,
|
|
payload: { owner: effect.owner, action: effect.action, wakeId },
|
|
};
|
|
}
|
|
if (effect.kind === "schedule_retry") {
|
|
failAt("continuation_materialization", input.failpoint);
|
|
if (effect.cause === NATIVE_PROVIDER_OVERLOADED_CODE) {
|
|
const retryId = await materializeNativeProviderCapacityRetry({
|
|
tx: input.tx as unknown as Db, companyId: input.companyId,
|
|
issueId: input.issue.id, runId: input.runId, agentId: effect.agentId,
|
|
});
|
|
return { effectKind: effect.kind, targetType: "heartbeat_run", targetId: retryId,
|
|
payload: { cause: effect.cause, summary: effect.summary } };
|
|
}
|
|
const wakeId = await enqueueWake({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
issueId: input.issue.id,
|
|
agentId: effect.agentId,
|
|
reason: "issue_status_changed",
|
|
idempotencyKey: `native-status:${input.decisionId}:retry`,
|
|
payload: {
|
|
nativeDecisionId: input.decisionId,
|
|
retryCause: effect.cause,
|
|
retrySummary: effect.summary,
|
|
},
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "agent_wakeup_request",
|
|
targetId: wakeId,
|
|
payload: { cause: effect.cause, summary: effect.summary },
|
|
};
|
|
}
|
|
if (effect.kind === "record_finalization_error") {
|
|
failAt("recovery_materialization", input.failpoint);
|
|
const recovery = await issueRecoveryActionService(
|
|
input.tx,
|
|
).upsertSourceScoped({
|
|
companyId: input.companyId,
|
|
sourceIssueId: input.issue.id,
|
|
kind: "active_run_watchdog",
|
|
ownerType: "agent",
|
|
ownerAgentId: effect.agentId,
|
|
cause: effect.cause,
|
|
fingerprint: nativeSha256({
|
|
runId: input.runId,
|
|
decisionId: input.decisionId,
|
|
cause: effect.cause,
|
|
}),
|
|
evidence: { runId: input.runId, decisionId: input.decisionId },
|
|
nextAction: effect.nextAction,
|
|
wakePolicy: null,
|
|
maxAttempts: 3,
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_recovery_action",
|
|
targetId: recovery.id,
|
|
payload: { cause: effect.cause, nextAction: effect.nextAction },
|
|
};
|
|
}
|
|
if (effect.kind === "release_run_resources") {
|
|
failAt("recovery_materialization", input.failpoint);
|
|
const [released] = await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
nativePhase: "committed",
|
|
nativePhaseUpdatedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({ id: heartbeatRuns.id });
|
|
if (!released) throw new Error("native_run_resources_not_released");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId: released.id,
|
|
payload: { processReleased: true },
|
|
};
|
|
}
|
|
if (effect.kind === "record_expiry") {
|
|
failAt("recovery_materialization", input.failpoint);
|
|
if (!input.issue.assigneeAgentId)
|
|
throw new Error("native_recovery_owner_missing");
|
|
const interaction = await input.tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issue.id),
|
|
inArray(issueThreadInteractions.status, ["pending", "expired"]),
|
|
),
|
|
)
|
|
.for("update")
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!interaction) throw new Error("native_attention_expiry_target_missing");
|
|
await input.tx
|
|
.update(issueThreadInteractions)
|
|
.set({
|
|
status: "expired",
|
|
resolvedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(issueThreadInteractions.id, interaction.id));
|
|
const cause = "attention_expired";
|
|
const recovery = await issueRecoveryActionService(
|
|
input.tx,
|
|
).upsertSourceScoped({
|
|
companyId: input.companyId,
|
|
sourceIssueId: input.issue.id,
|
|
kind: "active_run_watchdog",
|
|
ownerType: "agent",
|
|
ownerAgentId: input.issue.assigneeAgentId,
|
|
cause,
|
|
fingerprint: nativeSha256({
|
|
runId: input.runId,
|
|
decisionId: input.decisionId,
|
|
cause,
|
|
}),
|
|
evidence: { runId: input.runId, decisionId: input.decisionId },
|
|
nextAction:
|
|
"Review the expired attention route and choose a new authorized path.",
|
|
wakePolicy: null,
|
|
maxAttempts: 3,
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_thread_interaction",
|
|
targetId: interaction.id,
|
|
payload: { cause, recoveryActionId: recovery.id },
|
|
};
|
|
}
|
|
if (effect.kind === "schedule_reconciliation") {
|
|
failAt("recovery_materialization", input.failpoint);
|
|
if (!input.issue.assigneeAgentId)
|
|
throw new Error("native_recovery_owner_missing");
|
|
const cause = "native_reconciliation_required";
|
|
const recovery = await issueRecoveryActionService(
|
|
input.tx,
|
|
).upsertSourceScoped({
|
|
companyId: input.companyId,
|
|
sourceIssueId: input.issue.id,
|
|
kind: "active_run_watchdog",
|
|
ownerType: "agent",
|
|
ownerAgentId: input.issue.assigneeAgentId,
|
|
cause,
|
|
fingerprint: nativeSha256({
|
|
runId: input.runId,
|
|
decisionId: input.decisionId,
|
|
cause,
|
|
}),
|
|
evidence: { runId: input.runId, decisionId: input.decisionId },
|
|
nextAction:
|
|
"Reconcile the native decision against the latest issue status version.",
|
|
wakePolicy: null,
|
|
maxAttempts: 3,
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_recovery_action",
|
|
targetId: recovery.id,
|
|
payload: { cause },
|
|
};
|
|
}
|
|
if (effect.kind === "accept_replacement_turn") {
|
|
const [run] = await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
status: "running",
|
|
continuationAttempt: sql`${heartbeatRuns.continuationAttempt} + 1`,
|
|
nextAction: "Accept a replacement native turn on the existing run.",
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({
|
|
id: heartbeatRuns.id,
|
|
continuationAttempt: heartbeatRuns.continuationAttempt,
|
|
});
|
|
if (!run) throw new Error("native_replacement_turn_not_accepted");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId: run.id,
|
|
payload: { continuationAttempt: run.continuationAttempt },
|
|
};
|
|
}
|
|
if (effect.kind === "cancel_continuations") {
|
|
const wakeRows = await input.tx
|
|
.select({ id: agentWakeupRequests.id })
|
|
.from(agentWakeupRequests)
|
|
.where(
|
|
and(
|
|
eq(agentWakeupRequests.companyId, input.companyId),
|
|
inArray(agentWakeupRequests.status, [
|
|
"queued",
|
|
"deferred_issue_execution",
|
|
"claimed",
|
|
]),
|
|
sql`coalesce(
|
|
${agentWakeupRequests.payload} ->> 'issueId',
|
|
${agentWakeupRequests.payload} ->> 'taskId',
|
|
${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'issueId',
|
|
${agentWakeupRequests.payload} -> '_paperclipWakeContext' ->> 'taskId'
|
|
) = ${input.issue.id}`,
|
|
),
|
|
);
|
|
if (wakeRows.length > 0) {
|
|
await input.tx
|
|
.update(agentWakeupRequests)
|
|
.set({
|
|
status: "cancelled",
|
|
finishedAt: new Date(),
|
|
error: "Cancelled by native issue cancellation",
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
inArray(
|
|
agentWakeupRequests.id,
|
|
wakeRows.map((row) => row.id),
|
|
),
|
|
);
|
|
}
|
|
const interactionRows = await input.tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issue.id),
|
|
eq(issueThreadInteractions.status, "pending"),
|
|
),
|
|
);
|
|
if (interactionRows.length > 0) {
|
|
await input.tx
|
|
.update(issueThreadInteractions)
|
|
.set({
|
|
status: "cancelled",
|
|
resolvedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
inArray(
|
|
issueThreadInteractions.id,
|
|
interactionRows.map((row) => row.id),
|
|
),
|
|
);
|
|
}
|
|
await notifyExternalAgentSettlement(input.tx, { companyId: input.companyId, runId: input.runId });
|
|
await notifyInstructionCollection(input.tx, { companyId: input.companyId, runId: input.runId });
|
|
await notifyCancelledWorkspacePreservation(input.tx, { companyId: input.companyId, runId: input.runId });
|
|
await notifyUnsafeWorkspaceExportRecovery(input.tx, { companyId: input.companyId, issueId: input.issue.id });
|
|
await notifyDeliveryWork(input.tx, DELIVERY_QUEUES.accounting, input.companyId);
|
|
const [run] = await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
status: "cancelled",
|
|
finishedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning();
|
|
if (!run) throw new Error("native_continuation_cancellation_run_missing");
|
|
input.terminalRunsToEmit?.push(run);
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId: run.id,
|
|
payload: {
|
|
cancelledWakeIds: wakeRows.map((row) => row.id),
|
|
cancelledInteractionIds: interactionRows.map((row) => row.id),
|
|
},
|
|
};
|
|
}
|
|
if (effect.kind === "append_superseding_assessment") {
|
|
const lineage = await input.tx
|
|
.select({
|
|
currentAssessmentId: statusDecisions.assessmentId,
|
|
})
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.id, input.decisionId),
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!lineage) throw new Error("native_superseded_assessment_missing");
|
|
if (!input.issue.lastStatusDecisionId) {
|
|
const assessment = await input.tx
|
|
.select({
|
|
id: workAssessments.id,
|
|
supersedesAssessmentId: workAssessments.supersedesAssessmentId,
|
|
})
|
|
.from(workAssessments)
|
|
.where(
|
|
and(
|
|
eq(workAssessments.id, lineage.currentAssessmentId),
|
|
eq(workAssessments.companyId, input.companyId),
|
|
eq(workAssessments.issueId, input.issue.id),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!assessment?.supersedesAssessmentId) {
|
|
throw new Error("native_superseding_assessment_not_linked");
|
|
}
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "status_decision",
|
|
targetId: input.decisionId,
|
|
payload: {
|
|
supersedesDecisionId: null,
|
|
assessmentId: assessment.id,
|
|
supersedesAssessmentId: assessment.supersedesAssessmentId,
|
|
},
|
|
};
|
|
}
|
|
const prior = await input.tx
|
|
.select({
|
|
id: statusDecisions.id,
|
|
})
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.id, input.issue.lastStatusDecisionId),
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!prior) throw new Error("native_superseded_assessment_missing");
|
|
const assessment = await input.tx
|
|
.select({
|
|
id: workAssessments.id,
|
|
supersedesAssessmentId: workAssessments.supersedesAssessmentId,
|
|
})
|
|
.from(workAssessments)
|
|
.where(
|
|
and(
|
|
eq(workAssessments.id, lineage.currentAssessmentId),
|
|
eq(workAssessments.companyId, input.companyId),
|
|
eq(workAssessments.issueId, input.issue.id),
|
|
eq(workAssessments.runId, input.runId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!assessment?.supersedesAssessmentId) {
|
|
throw new Error("native_superseding_assessment_not_linked");
|
|
}
|
|
// Status decisions form one issue-wide sequence and may supersede a
|
|
// decision from another run. Work-assessment lineage is deliberately
|
|
// run-local, so retain the parent recorded when this reassessment was
|
|
// created instead of replacing it with the prior decision's assessment.
|
|
const [decision] = await input.tx
|
|
.update(statusDecisions)
|
|
.set({
|
|
supersedesDecisionId: input.issue.lastStatusDecisionId,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.id, input.decisionId),
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({ id: statusDecisions.id });
|
|
if (!decision) throw new Error("native_superseding_assessment_not_linked");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "status_decision",
|
|
targetId: decision.id,
|
|
payload: {
|
|
supersedesDecisionId: input.issue.lastStatusDecisionId,
|
|
assessmentId: assessment.id,
|
|
supersedesAssessmentId: assessment.supersedesAssessmentId,
|
|
},
|
|
};
|
|
}
|
|
if (effect.kind === "dispatch_pending_effect") {
|
|
const pending = await input.tx
|
|
.select({ id: statusDecisionEffects.id })
|
|
.from(statusDecisionEffects)
|
|
.where(
|
|
and(
|
|
eq(statusDecisionEffects.companyId, input.companyId),
|
|
eq(statusDecisionEffects.issueId, input.issue.id),
|
|
inArray(statusDecisionEffects.deliveryState, ["pending", "failed"]),
|
|
),
|
|
)
|
|
.for("update");
|
|
if (pending.length === 0) throw new Error("native_pending_effect_missing");
|
|
await notifyNativeFinalizationWork(input.tx, { companyId: input.companyId, issueId: input.issue.id });
|
|
await input.tx
|
|
.update(statusDecisionEffects)
|
|
.set({
|
|
deliveryState: "delivered",
|
|
attemptCount: sql`${statusDecisionEffects.attemptCount} + 1`,
|
|
deliveredAt: new Date(),
|
|
nextAttemptAt: null,
|
|
lastError: null,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
inArray(
|
|
statusDecisionEffects.id,
|
|
pending.map((row) => row.id),
|
|
),
|
|
);
|
|
await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeDispatchedEffectIds",
|
|
value: pending.map((row) => row.id),
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId: input.runId,
|
|
payload: { dispatchedEffectIds: pending.map((row) => row.id) },
|
|
};
|
|
}
|
|
if (effect.kind === "increment_status_version") {
|
|
const updated = await issueService(input.tx).update(
|
|
input.issue.id,
|
|
{
|
|
statusVersion: Number(input.issue.statusVersion) + 1,
|
|
actorAgentId: null,
|
|
actorUserId: null,
|
|
},
|
|
input.tx,
|
|
);
|
|
if (!updated) throw new Error("native_status_version_not_incremented");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue",
|
|
targetId: updated.id,
|
|
payload: { statusVersion: updated.statusVersion },
|
|
};
|
|
}
|
|
if (effect.kind === "record_shadow_decision") {
|
|
const targetId = await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeShadowDecision",
|
|
value: { decisionId: input.decisionId, applicationState: "shadow" },
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId,
|
|
payload: { decisionId: input.decisionId, applicationState: "shadow" },
|
|
};
|
|
}
|
|
if (effect.kind === "render_four_layers") {
|
|
const targetId = await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeOutcomeLayers",
|
|
value: {
|
|
runStatus: "persisted",
|
|
issueStatus: input.issue.status,
|
|
claim: "preserved",
|
|
decisionId: input.decisionId,
|
|
},
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId,
|
|
payload: { layerCount: 4 },
|
|
};
|
|
}
|
|
if (effect.kind === "materialize_contract") {
|
|
const contract = await input.tx
|
|
.select({
|
|
id: completionContracts.id,
|
|
canonicalSha256: completionContracts.canonicalSha256,
|
|
})
|
|
.from(completionContracts)
|
|
.where(
|
|
and(
|
|
eq(completionContracts.companyId, input.companyId),
|
|
eq(completionContracts.issueId, input.issue.id),
|
|
),
|
|
)
|
|
.orderBy(desc(completionContracts.revision))
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!contract) throw new Error("native_completion_contract_missing");
|
|
await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
completionContractId: contract.id,
|
|
completionContractSha256: contract.canonicalSha256,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
);
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "completion_contract",
|
|
targetId: contract.id,
|
|
payload: { canonicalSha256: contract.canonicalSha256 },
|
|
};
|
|
}
|
|
if (effect.kind === "record_mode_labeled_divergence") {
|
|
const targetId = await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeLegacyDivergence",
|
|
value: { mode: "native", decisionId: input.decisionId, classified: true },
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId,
|
|
payload: { mode: "native", classified: true },
|
|
};
|
|
}
|
|
if (effect.kind === "record_mode_native") {
|
|
const [run] = await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
runtimeMode: "native",
|
|
runtimeModeResolvedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.id, input.runId),
|
|
eq(heartbeatRuns.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({ id: heartbeatRuns.id });
|
|
if (!run) throw new Error("native_runtime_mode_not_recorded");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId: run.id,
|
|
payload: { runtimeMode: "native" },
|
|
};
|
|
}
|
|
if (effect.kind === "record_policy_version") {
|
|
const targetId = await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeStatusPolicyVersion",
|
|
value: NATIVE_STATUS_ARBITER_POLICY_VERSION,
|
|
runnerProfile: true,
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId,
|
|
payload: { policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION },
|
|
};
|
|
}
|
|
if (effect.kind === "finish_as_native") {
|
|
const targetId = await updateRunEffectState({
|
|
tx: input.tx,
|
|
companyId: input.companyId,
|
|
runId: input.runId,
|
|
key: "nativeKillSwitchDisposition",
|
|
value: "finish_as_native",
|
|
});
|
|
await input.tx
|
|
.update(heartbeatRuns)
|
|
.set({ runtimeMode: "native", updatedAt: new Date() })
|
|
.where(eq(heartbeatRuns.id, input.runId));
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "heartbeat_run",
|
|
targetId,
|
|
payload: { runtimeMode: "native" },
|
|
};
|
|
}
|
|
if (effect.kind === "resume_workspace_operation") {
|
|
const resumed = input.preMaterializedEffects?.get(effect.kind);
|
|
if (
|
|
!resumed ||
|
|
resumed.targetType !== "workspace_operation" ||
|
|
!resumed.targetId
|
|
) {
|
|
throw new Error("native_workspace_operation_not_executed");
|
|
}
|
|
const operation = await input.tx
|
|
.select({ status: workspaceOperations.status })
|
|
.from(workspaceOperations)
|
|
.where(
|
|
and(
|
|
eq(workspaceOperations.id, resumed.targetId),
|
|
eq(workspaceOperations.companyId, input.companyId),
|
|
eq(workspaceOperations.heartbeatRunId, input.runId),
|
|
eq(workspaceOperations.issueId, input.issue.id),
|
|
eq(workspaceOperations.phase, "workspace_finalize"),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (operation?.status !== "succeeded")
|
|
throw new Error("native_workspace_operation_not_succeeded");
|
|
return resumed;
|
|
}
|
|
if (effect.kind === "record_stale_response") {
|
|
const interaction = await input.tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issue.id),
|
|
inArray(issueThreadInteractions.status, [
|
|
"answered",
|
|
"accepted",
|
|
"rejected",
|
|
"expired",
|
|
]),
|
|
),
|
|
)
|
|
.orderBy(desc(issueThreadInteractions.updatedAt))
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!interaction) throw new Error("native_stale_response_missing");
|
|
await input.tx
|
|
.update(issueThreadInteractions)
|
|
.set({
|
|
summary: "Response retained for audit after native supersession.",
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(issueThreadInteractions.id, interaction.id));
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_thread_interaction",
|
|
targetId: interaction.id,
|
|
payload: { status: "superseded" },
|
|
};
|
|
}
|
|
if (effect.kind === "link_canonical_request") {
|
|
const interactions = await input.tx
|
|
.select({ id: issueThreadInteractions.id })
|
|
.from(issueThreadInteractions)
|
|
.where(
|
|
and(
|
|
eq(issueThreadInteractions.companyId, input.companyId),
|
|
eq(issueThreadInteractions.issueId, input.issue.id),
|
|
),
|
|
)
|
|
.orderBy(asc(issueThreadInteractions.createdAt))
|
|
.limit(2);
|
|
if (interactions.length < 2)
|
|
throw new Error("native_attention_canonical_pair_missing");
|
|
const canonical = interactions[0]!;
|
|
const duplicate = interactions[1]!;
|
|
await input.tx
|
|
.update(issueThreadInteractions)
|
|
.set({
|
|
summary: `Canonical native attention request: ${canonical.id}`,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(issueThreadInteractions.id, duplicate.id));
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_thread_interaction",
|
|
targetId: duplicate.id,
|
|
payload: { canonicalRequestId: canonical.id },
|
|
};
|
|
}
|
|
if (effect.kind === "release_checkout") {
|
|
await notifyUnsafeWorkspaceExportRecovery(input.tx, { companyId: input.companyId, issueId: input.issue.id });
|
|
const [released] = await input.tx
|
|
.update(issues)
|
|
.set({
|
|
checkoutRunId: null,
|
|
executionRunId: null,
|
|
executionAgentNameKey: null,
|
|
executionLockedAt: null,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.issue.id),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.returning({ id: issues.id });
|
|
if (!released) throw new Error("native_checkout_not_released");
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_checkout",
|
|
targetId: released.id,
|
|
payload: { checkoutRunId: null, executionRunId: null },
|
|
};
|
|
}
|
|
if (effect.kind === "record_recovery") {
|
|
failAt("recovery_materialization", input.failpoint);
|
|
const recovery = await issueRecoveryActionService(
|
|
input.tx,
|
|
).upsertSourceScoped({
|
|
companyId: input.companyId,
|
|
sourceIssueId: input.issue.id,
|
|
kind: "active_run_watchdog",
|
|
ownerType: "agent",
|
|
ownerAgentId: effect.agentId,
|
|
cause: effect.cause,
|
|
fingerprint: nativeSha256({
|
|
runId: input.runId,
|
|
decisionId: input.decisionId,
|
|
cause: effect.cause,
|
|
}),
|
|
evidence: { runId: input.runId, decisionId: input.decisionId },
|
|
nextAction: effect.nextAction,
|
|
wakePolicy: { kind: "resume_native_run", runId: input.runId },
|
|
maxAttempts: 3,
|
|
});
|
|
return {
|
|
effectKind: effect.kind,
|
|
targetType: "issue_recovery_action",
|
|
targetId: recovery.id,
|
|
payload: { cause: effect.cause, nextAction: effect.nextAction },
|
|
};
|
|
}
|
|
return assertNeverEffect(effect);
|
|
}
|
|
|
|
export async function commitNativeStatusDecision(input: {
|
|
db: Db;
|
|
companyId: string;
|
|
issueId: string;
|
|
runId: string;
|
|
assessmentId: string;
|
|
priorStatus: string;
|
|
priorStatusVersion: number;
|
|
priorDecisionId: string | null;
|
|
decision: NativeStatusDecision;
|
|
failpoint?: NativeStatusCommitFailpoint;
|
|
preMaterializedEffects?: NativeMaterializedStatusEffect[];
|
|
supersedesCommittedDecisionId?: string;
|
|
supersededExternalReviewRequests?: { agentId: string; resultId: string; interactionIds: string[] };
|
|
requireExternalChatResponseWaitAuthorization?: { agentId: string; completionContinuationKey?: string };
|
|
requirePlanWaitSource?: NativePlanWaitSource;
|
|
requireMonitorWait?: { agentId: string; nextCheckAt: string };
|
|
requireMonitorDelivery?: NativeMonitorDelivery;
|
|
requirePreservedMonitorWait?: NativePreservedMonitorWait;
|
|
requireNoPendingChildCompletion?: NativeChildCompletionRecipient;
|
|
requireReassessmentSource?: {
|
|
phase: string; resultId: string; assessmentId: string; decisionId: string | null; agentId: string;
|
|
retiredAutomaticReview: { contractId: string; governanceGate: NativeGovernanceGate | null; hasUnresolvedIssueBlockers: boolean } | null;
|
|
};
|
|
requireModelRejectionOwner?: { agentId: string; reviewContext: NativeReviewAssignmentContext | null };
|
|
requireProviderFailureOwner?: { agentId: string; reviewContext: NativeReviewAssignmentContext | null };
|
|
requireBoardResponseWaitSource?: NativeBoardResponseWaitSource;
|
|
requireBoardResponseWaitOrigin?: NativeBoardResponseWaitOrigin;
|
|
reviewResponsePresentation?: {
|
|
agentId: string;
|
|
resultId: string;
|
|
gateId: string;
|
|
};
|
|
}) {
|
|
if (input.decision.reasonCode === null) {
|
|
throw new Error("native_status_reason_code_required");
|
|
}
|
|
const reasonCode = input.decision.reasonCode;
|
|
if (reasonCode === "native_provider_model_rejected" && !input.requireModelRejectionOwner) {
|
|
throw new Error("native_model_rejection_owner_required");
|
|
}
|
|
if (reasonCode.startsWith("native_provider_overloaded") && !input.requireProviderFailureOwner) {
|
|
throw new Error("native_provider_failure_owner_required");
|
|
}
|
|
const publications: ActivityPublication[] = [];
|
|
const terminalRunsToEmit: (typeof heartbeatRuns.$inferSelect)[] = [];
|
|
const committed = await input.db.transaction(async (tx) => {
|
|
const retiredReview = input.requireReassessmentSource?.retiredAutomaticReview;
|
|
if (retiredReview && !await lockCurrentNativeCompletionContract(tx as unknown as Db, {
|
|
companyId: input.companyId, issueId: input.issueId, contractId: retiredReview.contractId,
|
|
})) throw new NativeStatusRaceError();
|
|
if (retiredReview && !await tryLockNativeWorkspaceFinalization(tx as unknown as Db, input)) throw new NativeStatusRaceError();
|
|
const coordinator = await tx
|
|
.select()
|
|
.from(nativeRunFinalizations)
|
|
.where(
|
|
and(
|
|
eq(nativeRunFinalizations.runId, input.runId),
|
|
eq(nativeRunFinalizations.companyId, input.companyId),
|
|
eq(nativeRunFinalizations.issueId, input.issueId),
|
|
),
|
|
)
|
|
.for("update")
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!coordinator)
|
|
throw new Error("native_finalization_coordinator_missing");
|
|
const issue = await tx
|
|
.select()
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.issueId),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.for("update")
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!issue) throw new NativeStatusRaceError();
|
|
if (input.requireReassessmentSource) {
|
|
const expected = input.requireReassessmentSource;
|
|
const now = Date.now();
|
|
if (coordinator.phase === "terminal_failure" || coordinator.phase !== expected.phase ||
|
|
coordinator.resultId !== expected.resultId || coordinator.assessmentId !== expected.assessmentId ||
|
|
coordinator.decisionId !== expected.decisionId ||
|
|
(coordinator.nextAttemptAt !== null && coordinator.nextAttemptAt.getTime() > now) ||
|
|
(coordinator.leaseOwner !== null &&
|
|
(coordinator.leaseExpiresAt === null || coordinator.leaseExpiresAt.getTime() > now))) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
const run = await tx.select().from(heartbeatRuns).where(and(
|
|
eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.companyId),
|
|
eq(heartbeatRuns.nativeIssueId, input.issueId), eq(heartbeatRuns.agentId, expected.agentId),
|
|
)).for("update").limit(1).then(rows => rows[0]);
|
|
if (!run || run.runtimeMode !== "native" || isNativeRunnerOwnershipHeld(run)) throw new NativeStatusRaceError();
|
|
if (expected.retiredAutomaticReview && (run.status !== "succeeded" ||
|
|
hasNativeWorkspaceFinalizationOwner(run.runnerProfileJson) ||
|
|
(issue.executionRunId !== null && issue.executionRunId !== input.runId))) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
if (expected.retiredAutomaticReview) {
|
|
try {
|
|
const [barrier] = await tx.select({ status: workspaceOperations.status }).from(workspaceOperations)
|
|
.where(and(eq(workspaceOperations.companyId, input.companyId),
|
|
eq(workspaceOperations.heartbeatRunId, input.runId), eq(workspaceOperations.phase, "workspace_finalize")))
|
|
.orderBy(desc(workspaceOperations.createdAt)).limit(1).for("share", { noWait: true });
|
|
if (barrier && barrier.status !== "succeeded") throw new NativeStatusRaceError();
|
|
const executionState = record(issue.executionState);
|
|
if (executionState.status !== "pending") await lockNativeGovernanceSources({ db: tx as unknown as Db,
|
|
companyId: input.companyId, issueId: input.issueId });
|
|
const gate = await pendingNativeGovernance({ db: tx as unknown as Db, companyId: input.companyId,
|
|
issueId: input.issueId, runId: input.runId, executionState });
|
|
const expectedGate = expected.retiredAutomaticReview.governanceGate;
|
|
if (gate?.kind !== expectedGate?.kind || gate?.id !== expectedGate?.id) throw new NativeStatusRaceError();
|
|
const readiness = await issueService(tx as unknown as Db).getDependencyReadiness(input.issueId, tx, { lockSources: true });
|
|
if ((readiness.unresolvedBlockerCount > 0) !== expected.retiredAutomaticReview.hasUnresolvedIssueBlockers) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
} catch (error) {
|
|
if (record(error).code === "55P03" || record(record(error).cause).code === "55P03") throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
}
|
|
const pendingEffect = expected.decisionId ? await tx.select({ id: statusDecisionEffects.id })
|
|
.from(statusDecisionEffects).where(and(
|
|
eq(statusDecisionEffects.companyId, input.companyId), eq(statusDecisionEffects.issueId, input.issueId),
|
|
eq(statusDecisionEffects.decisionId, expected.decisionId),
|
|
inArray(statusDecisionEffects.deliveryState, ["pending", "failed"]),
|
|
)).limit(1).then(rows => rows[0]) : null;
|
|
if (pendingEffect) throw new NativeStatusRaceError();
|
|
try {
|
|
if (!await lockCurrentNativeWorkAssessmentEvidence(tx as unknown as Db, {
|
|
companyId: input.companyId, issueId: input.issueId, runId: input.runId,
|
|
resultId: expected.resultId, assessmentId: input.assessmentId,
|
|
})) throw new NativeStatusRaceError();
|
|
} catch (error) {
|
|
if (record(error).code === "55P03" || record(record(error).cause).code === "55P03") throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
}
|
|
// A completed model turn cannot close a persistent conversation. Preserve
|
|
// the task here; the response finalizer records its durable waiting state.
|
|
if (issue.conversationAgentId && input.decision.statusAction === "done") {
|
|
input = { ...input, decision: { ...input.decision, statusAction: "preserve", toStatus: issue.status as NativeStatusDecision["toStatus"], effects: [] } };
|
|
}
|
|
if (coordinator.phase === "committed" && coordinator.decisionId) {
|
|
if (input.supersedesCommittedDecisionId) {
|
|
if (
|
|
coordinator.decisionId !== input.supersedesCommittedDecisionId ||
|
|
coordinator.assessmentId === input.assessmentId
|
|
) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
} else {
|
|
const existingDecision = await tx
|
|
.select()
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.id, coordinator.decisionId),
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
eq(statusDecisions.issueId, input.issueId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (!existingDecision)
|
|
throw new Error("native_committed_decision_missing");
|
|
return { decision: existingDecision, issue, replayed: true };
|
|
}
|
|
}
|
|
if (
|
|
issue.status !== input.priorStatus ||
|
|
Number(issue.statusVersion) !== input.priorStatusVersion ||
|
|
issue.lastStatusDecisionId !== input.priorDecisionId
|
|
) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
// Recheck under the issue lock, retaining run/proposal/document proof until
|
|
// commit. A newer decision cannot race an earlier first-task Done assessment.
|
|
if (input.decision.statusAction === "done" && !await onboardingFirstTaskCompletionAllowed(tx as unknown as Db, {
|
|
companyId: input.companyId, issueId: input.issueId, runId: input.runId,
|
|
})) throw new NativeStatusRaceError();
|
|
if (reasonCode === "scheduled_monitor_waiting") {
|
|
const expected = input.requireMonitorWait;
|
|
const inReview = input.decision.statusAction === "in_review" && input.decision.toStatus === "in_review";
|
|
let publishedReviewWait = inReview && await hasPublishedGitHubReview(tx as unknown as Db, input);
|
|
if (expected && inReview && !publishedReviewWait) {
|
|
// Arbitration may reuse a previous publication. Prove that same source
|
|
// here, while retaining the independently saved monitor and its owner.
|
|
const [run] = await tx.select({
|
|
id: heartbeatRuns.id, companyId: heartbeatRuns.companyId, agentId: heartbeatRuns.agentId,
|
|
responsibleUserId: heartbeatRuns.responsibleUserId,
|
|
activeIdentityContextId: heartbeatRuns.activeIdentityContextId,
|
|
nativeIssueId: heartbeatRuns.nativeIssueId, issueId: heartbeatRuns.issueId,
|
|
}).from(heartbeatRuns).where(and(
|
|
eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.companyId),
|
|
eq(heartbeatRuns.nativeIssueId, input.issueId), eq(heartbeatRuns.agentId, expected.agentId),
|
|
)).limit(1);
|
|
publishedReviewWait = !!run && (await readInheritedGitHubReviewWait(tx as unknown as Db,
|
|
{ ...run, contextSnapshot: null }))?.kind === "published";
|
|
}
|
|
if (!expected || issue.workMode !== "standard" || issue.executionRunId !== input.runId ||
|
|
(input.decision.statusAction !== "preserve" && !publishedReviewWait) || input.decision.effects.length !== 1 ||
|
|
input.decision.effects[0]?.kind !== "release_checkout" ||
|
|
scheduledIssueMonitorWait(issue, expected.agentId) !== expected.nextCheckAt) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
}
|
|
if (reasonCode === "scheduled_monitor_dispatched") {
|
|
const expected = input.requireMonitorDelivery;
|
|
const actual = expected ? await readNativeMonitorDelivery(tx as unknown as Db, input) : null;
|
|
if (!expected || !actual || actual.wakeId !== expected.wakeId ||
|
|
actual.successorRunId !== expected.successorRunId ||
|
|
issue.executionRunId === input.runId || issue.checkoutRunId === input.runId ||
|
|
input.decision.statusAction !== "preserve" || input.decision.toStatus !== issue.status ||
|
|
input.decision.effects.length !== 0) throw new NativeStatusRaceError();
|
|
}
|
|
if (reasonCode === "scheduled_monitor_preserved") {
|
|
const expected = input.requirePreservedMonitorWait;
|
|
const actual = expected ? await readNativePreservedMonitorWait(tx as unknown as Db, {
|
|
companyId: input.companyId, issueId: input.issueId, runId: input.runId, agentId: expected.agentId,
|
|
}, issue) : null;
|
|
if (!expected || !actual || actual.nextCheckAt !== expected.nextCheckAt || actual.resultId !== expected.resultId ||
|
|
input.decision.statusAction !== "preserve" || input.decision.toStatus !== issue.status ||
|
|
input.decision.effects.length !== 0 || await pendingNativeGovernance({ db: tx as unknown as Db,
|
|
companyId: input.companyId, issueId: input.issueId, runId: input.runId, executionState: record(issue.executionState),
|
|
})) throw new NativeStatusRaceError();
|
|
await notifyDeliveryWork(tx, DELIVERY_QUEUES.issueMonitors);
|
|
}
|
|
// Recheck under the parent status lock: a child can finish between the
|
|
// finalizer's read and this commit. Re-arbitrate instead of discarding its wake.
|
|
if (input.requireNoPendingChildCompletion && await hasPendingNativeChildCompletion(
|
|
tx as unknown as Db, input.requireNoPendingChildCompletion,
|
|
)) throw new NativeStatusRaceError();
|
|
if (input.requireModelRejectionOwner || input.requireProviderFailureOwner) {
|
|
const owner = (input.requireProviderFailureOwner ?? input.requireModelRejectionOwner)!;
|
|
// A successor can claim execution without changing statusVersion. Keep
|
|
// this new blocking authority behind the same locked issue snapshot.
|
|
if (issue.executionRunId && issue.executionRunId !== input.runId) throw new NativeStatusRaceError();
|
|
if (owner.reviewContext) {
|
|
const review = await getNativeReviewAssignment(tx as unknown as Db, {
|
|
companyId: input.companyId, issueId: input.issueId, agentId: owner.agentId, contextSnapshot: owner.reviewContext,
|
|
});
|
|
if (review?.interaction.status !== "pending") throw new NativeStatusRaceError();
|
|
} else if (issue.assigneeAgentId !== owner.agentId || issue.assigneeUserId) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
}
|
|
if (input.requireExternalChatResponseWaitAuthorization) {
|
|
let authorization;
|
|
try {
|
|
authorization =
|
|
await resolveExternalChatResponseWaitAuthorizationInTransaction(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
agentId:
|
|
input.requireExternalChatResponseWaitAuthorization.agentId,
|
|
},
|
|
"nonblocking",
|
|
);
|
|
} catch (error) {
|
|
if (isExternalChatWaitAuthorizationContention(error)) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
throw error;
|
|
}
|
|
if (authorization !== "authorized") {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
if (input.requireExternalChatResponseWaitAuthorization.completionContinuationKey) {
|
|
const current = await readPublishedGitHubReviewCompletionContinuationInTransaction(tx as unknown as Db,
|
|
{ companyId: input.companyId, issueId: input.issueId, runId: input.runId,
|
|
agentId: input.requireExternalChatResponseWaitAuthorization.agentId }, "nonblocking");
|
|
if (current?.idempotencyKey !== input.requireExternalChatResponseWaitAuthorization.completionContinuationKey)
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
}
|
|
const passiveBoardResponseWait =
|
|
reasonCode === "board_response_waiting" ||
|
|
reasonCode === "board_response_wait_superseded";
|
|
let planWait: Awaited<ReturnType<typeof readNativePlanWait>> = null;
|
|
let planWaitResult: { resultId: string; resultSha256: string } | null = null;
|
|
if (reasonCode === "native_plan_accepted_waiting_for_continuation") {
|
|
const expected = input.requirePlanWaitSource;
|
|
if (!expected || expected.companyId !== input.companyId || expected.issueId !== input.issueId || expected.runId !== input.runId || input.decision.effects.length !== 0 || input.decision.statusAction !== "in_progress" || input.decision.toStatus !== "in_progress") throw new NativeStatusRaceError();
|
|
try {
|
|
planWait = await readNativePlanWait(tx as unknown as Db, expected, true);
|
|
if (!planWait || nativeSha256(planWait.source) !== nativeSha256(expected)) throw new NativeStatusRaceError();
|
|
// Recheck the committed semantic result too: an arbitrary caller cannot
|
|
// use a genuine plan receipt to authorize a different finalization.
|
|
const [accepted] = await tx.select().from(nativeRunResults).where(and(
|
|
eq(nativeRunResults.id, coordinator!.resultId!), eq(nativeRunResults.companyId, input.companyId),
|
|
eq(nativeRunResults.issueId, input.issueId), eq(nativeRunResults.runId, input.runId), eq(nativeRunResults.schemaStatus, "accepted"),
|
|
)).for("share", { noWait: true });
|
|
const envelope = record(accepted?.resultJson), terminal = record(envelope.terminal);
|
|
if (!accepted || accepted.completionContractId !== planWait.source.contractId || accepted.turnId !== planWait.source.turnId ||
|
|
nativeSha256(envelope.result) !== nativeSha256(planWait.result) ||
|
|
terminal.schema !== "paperclip.prp.terminal.v1" || terminal.runTerminalState !== "succeeded" || terminal.turnTerminalState !== "completed" || terminal.reportedWorkDisposition !== "yielded") throw new NativeStatusRaceError();
|
|
// completeRun hashes its full private binding, not the stored resultJson.
|
|
// Preserve that accepted identity instead of inventing a new digest.
|
|
planWaitResult = { resultId: accepted.id, resultSha256: accepted.canonicalSha256 };
|
|
} catch (error) {
|
|
if (isExternalChatWaitAuthorizationContention(error)) throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
}
|
|
let boardResponseWaitOrigin: NativeBoardResponseWaitOrigin | null = null;
|
|
if (
|
|
passiveBoardResponseWait ||
|
|
input.requireBoardResponseWaitOrigin
|
|
) {
|
|
const expected = input.requireBoardResponseWaitOrigin;
|
|
if (
|
|
!expected ||
|
|
expected.companyId !== input.companyId ||
|
|
expected.issueId !== input.issueId ||
|
|
expected.runId !== input.runId ||
|
|
(passiveBoardResponseWait && input.decision.effects.length !== 0)
|
|
)
|
|
throw new NativeStatusRaceError();
|
|
try {
|
|
boardResponseWaitOrigin = await readNativeBoardResponseWaitOrigin(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
agentId: expected.agentId,
|
|
},
|
|
true,
|
|
);
|
|
} catch (error) {
|
|
if (isExternalChatWaitAuthorizationContention(error))
|
|
throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
if (
|
|
!boardResponseWaitOrigin ||
|
|
nativeSha256(boardResponseWaitOrigin) !== nativeSha256(expected)
|
|
)
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
let boardResponseWait: Awaited<
|
|
ReturnType<typeof readNativeBoardResponseWaitSource>
|
|
> = null;
|
|
// Corrective continuations also carry this precondition. Revalidate it
|
|
// under the same locks before any decision effects can restart old work.
|
|
if (reasonCode === "board_response_waiting" || input.requireBoardResponseWaitSource) {
|
|
const expected = input.requireBoardResponseWaitSource;
|
|
if (
|
|
!expected ||
|
|
(passiveBoardResponseWait && input.decision.effects.length !== 0) ||
|
|
expected.companyId !== input.companyId ||
|
|
expected.issueId !== input.issueId ||
|
|
expected.runId !== input.runId
|
|
)
|
|
throw new NativeStatusRaceError();
|
|
try {
|
|
boardResponseWait = await readNativeBoardResponseWaitSource(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
agentId: expected.agentId,
|
|
},
|
|
true,
|
|
);
|
|
} catch (error) {
|
|
if (isExternalChatWaitAuthorizationContention(error))
|
|
throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
if (
|
|
!boardResponseWait ||
|
|
nativeSha256(boardResponseWait.source) !== nativeSha256(expected)
|
|
) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
}
|
|
let externalChatReviewPresentation = null;
|
|
if (
|
|
reasonCode === "governed_response_waiting" &&
|
|
input.reviewResponsePresentation &&
|
|
input.decision.effects.length === 1 &&
|
|
input.decision.effects[0]?.kind === "create_interaction" &&
|
|
input.decision.effects[0].gate?.kind === "interaction" &&
|
|
input.decision.effects[0].gate.id ===
|
|
input.reviewResponsePresentation.gateId
|
|
) {
|
|
try {
|
|
externalChatReviewPresentation =
|
|
await prepareNativeChatReviewPresentationInTransaction(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
assessmentId: input.assessmentId,
|
|
...input.reviewResponsePresentation,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
if (isExternalChatWaitAuthorizationContention(error))
|
|
throw new NativeStatusRaceError();
|
|
throw error;
|
|
}
|
|
}
|
|
const recordCoordinatorDecision = async (decisionId: string) => {
|
|
const finalizationError = input.decision.effects.find(
|
|
effect => effect.kind === "record_finalization_error",
|
|
);
|
|
await notifyDeliveryWork(tx, DELIVERY_QUEUES.accounting, input.companyId);
|
|
await notifyNativeFinalizationWork(tx, input);
|
|
await tx
|
|
.update(nativeRunFinalizations)
|
|
.set({
|
|
phase: finalizationError ? "retryable_failure" : "committed",
|
|
assessmentId: input.assessmentId,
|
|
decisionId,
|
|
leaseOwner: null,
|
|
leaseExpiresAt: null,
|
|
recoveryState: null,
|
|
recoveryRequestId: null,
|
|
failureCode: finalizationError?.cause ?? null,
|
|
failureDetail: finalizationError
|
|
? {
|
|
originalFailureCode: finalizationError.cause,
|
|
recoveryOwner: {
|
|
kind: "agent",
|
|
agentId: finalizationError.agentId,
|
|
},
|
|
nextAction: finalizationError.nextAction,
|
|
}
|
|
: null,
|
|
nextAttemptAt: finalizationError ? new Date(Date.now() + 30_000) : null,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(nativeRunFinalizations.runId, input.runId));
|
|
};
|
|
const decisionJson = {
|
|
statusAction: input.decision.statusAction,
|
|
toStatus: input.decision.toStatus,
|
|
reasonCode,
|
|
unblockDescriptor: input.decision.unblockDescriptor,
|
|
effects: input.decision.effects,
|
|
...(externalChatReviewPresentation
|
|
? { externalChatReviewPresentation }
|
|
: {}),
|
|
...(boardResponseWait
|
|
? { boardResponseWait: boardResponseWait.source }
|
|
: {}),
|
|
...(boardResponseWaitOrigin ? { boardResponseWaitOrigin } : {}),
|
|
...(planWait ? { planWait: planWait.source, planWaitResult } : {}),
|
|
...(reasonCode === "scheduled_monitor_dispatched" ? { monitorDelivery: input.requireMonitorDelivery } : {}),
|
|
priorStatusVersion: input.priorStatusVersion,
|
|
projectedStatusVersion:
|
|
input.decision.statusAction === "preserve"
|
|
? input.priorStatusVersion
|
|
: input.priorStatusVersion + 1,
|
|
};
|
|
const decisionDigest = nativeSha256({
|
|
issueId: input.issueId,
|
|
assessmentId: input.assessmentId,
|
|
policyVersion: input.decision.policyVersion,
|
|
fromStatus: issue.status,
|
|
priorStatusVersion: input.priorStatusVersion,
|
|
priorDecisionId: input.priorDecisionId,
|
|
decision: decisionJson,
|
|
});
|
|
let decisionRow = await tx
|
|
.select()
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.issueId, input.issueId),
|
|
eq(statusDecisions.decisionDigest, decisionDigest),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null);
|
|
if (
|
|
decisionRow?.applicationState === "applied" ||
|
|
decisionRow?.applicationState === "proposed"
|
|
) {
|
|
// The effects already committed, but this retry owns a fresh coordinator
|
|
// lease. Release it and restore the retry deadline without replaying effects.
|
|
await recordCoordinatorDecision(decisionRow.id);
|
|
return { decision: decisionRow, issue, replayed: true };
|
|
}
|
|
await notifyChatRunMilestones(tx, input);
|
|
await notifyObsoletePolicyReviews(tx, input);
|
|
await notifyAutomaticCompletionReviews(tx, input);
|
|
if (!decisionRow) {
|
|
const latestDecisionVersion = await tx
|
|
.select({
|
|
decisionVersion: statusDecisions.decisionVersion,
|
|
})
|
|
.from(statusDecisions)
|
|
.where(
|
|
and(
|
|
eq(statusDecisions.companyId, input.companyId),
|
|
eq(statusDecisions.issueId, input.issueId),
|
|
),
|
|
)
|
|
.orderBy(desc(statusDecisions.decisionVersion))
|
|
.limit(1)
|
|
.then((rows) => Number(rows[0]?.decisionVersion ?? 0));
|
|
[decisionRow] = await tx
|
|
.insert(statusDecisions)
|
|
.values({
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
assessmentId: input.assessmentId,
|
|
decisionVersion:
|
|
Math.max(input.priorStatusVersion, latestDecisionVersion) + 1,
|
|
policyVersion: input.decision.policyVersion,
|
|
fromStatus: issue.status,
|
|
toStatus: input.decision.toStatus,
|
|
reasonCode,
|
|
decisionJson,
|
|
decisionDigest,
|
|
applicationState: "proposed",
|
|
})
|
|
.returning();
|
|
}
|
|
if (!decisionRow) throw new Error("native_status_decision_not_persisted");
|
|
|
|
if (planWait) {
|
|
await issueService(tx as unknown as Db).addComment(
|
|
input.issueId, planWait.result.summary,
|
|
{ agentId: planWait.source.agentId, runId: input.runId }, undefined, tx,
|
|
);
|
|
}
|
|
if (boardResponseWait && reasonCode === "board_response_waiting") {
|
|
// The answer and passive-wait receipt commit together. In particular,
|
|
// no chat presentation authorization is supplied: this is Board-only.
|
|
await issueService(tx as unknown as Db).addComment(
|
|
input.issueId,
|
|
boardResponseWait.summary,
|
|
{ agentId: boardResponseWait.source.agentId, runId: input.runId },
|
|
undefined,
|
|
tx,
|
|
);
|
|
}
|
|
|
|
if (input.decision.statusAction === "done" && issue.status !== "done" &&
|
|
issue.parentId && issue.originKind !== "task_watchdog") {
|
|
// Serialize child notification creation with the parent's final pending-
|
|
// result check. Lock before child writes can acquire an implicit parent
|
|
// foreign-key lock, so concurrent siblings never upgrade shared locks.
|
|
await tx.select({ id: issues.id }).from(issues).where(and(
|
|
eq(issues.id, issue.parentId), eq(issues.companyId, input.companyId),
|
|
)).for("update");
|
|
}
|
|
|
|
if (input.supersededExternalReviewRequests) {
|
|
const expected = input.supersededExternalReviewRequests;
|
|
const binding = { companyId: input.companyId, issueId: input.issueId, runId: input.runId, agentId: expected.agentId };
|
|
const candidates = await findSupersededExternalReviewRequests(tx as unknown as Db, binding);
|
|
const ids = candidates.map(({ interaction }) => interaction.id);
|
|
const [accepted] = await tx.select().from(nativeRunResults).where(and(
|
|
eq(nativeRunResults.id, coordinator.resultId!), eq(nativeRunResults.companyId, input.companyId),
|
|
eq(nativeRunResults.issueId, input.issueId), eq(nativeRunResults.runId, input.runId), eq(nativeRunResults.schemaStatus, "accepted"),
|
|
));
|
|
const result = record(accepted?.resultJson.result), terminal = record(accepted?.resultJson.terminal);
|
|
if (ids.length !== expected.interactionIds.length || expected.interactionIds.some(id => !ids.includes(id)) ||
|
|
terminal.runTerminalState !== "succeeded" || terminal.turnTerminalState !== "completed" ||
|
|
!["done", "yielded"].includes(String(result.reportedWorkDisposition)) ||
|
|
!accepted || accepted.id !== expected.resultId || normalizePrpResultSignals(accepted.resultJson.result as PrpStructuredRunResult).actionableAttentionRequests.length !== 0 ||
|
|
!["completion_claim_policy_accepted", "completion_contract_satisfied", "external_chat_response_waiting", "scheduled_monitor_waiting"].includes(reasonCode) ||
|
|
await pendingNativeGovernance({ db: tx as unknown as Db, ...binding, executionState: record(issue.executionState), ignoredInteractionIds: ids })) {
|
|
throw new NativeStatusRaceError();
|
|
}
|
|
await supersedeExternalReviewRequestsInTransaction(tx as unknown as Db, binding, candidates, publications);
|
|
}
|
|
|
|
const materialized: NativeMaterializedStatusEffect[] = [];
|
|
const preMaterializedEffects = new Map(
|
|
(input.preMaterializedEffects ?? []).map((effect) => [
|
|
effect.effectKind,
|
|
effect,
|
|
]),
|
|
);
|
|
for (const effect of input.decision.effects) {
|
|
materialized.push(
|
|
await materializeDecisionEffect({
|
|
tx: tx as unknown as Db,
|
|
companyId: input.companyId,
|
|
issue,
|
|
runId: input.runId,
|
|
decisionId: decisionRow.id,
|
|
effect,
|
|
failpoint: input.failpoint,
|
|
preMaterializedEffects,
|
|
terminalRunsToEmit,
|
|
}),
|
|
);
|
|
}
|
|
|
|
let updated: typeof issues.$inferSelect;
|
|
if (input.decision.statusAction === "preserve") {
|
|
updated = (await tx
|
|
.select()
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.id, input.issueId),
|
|
eq(issues.companyId, input.companyId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => rows[0] ?? null)) as typeof issues.$inferSelect;
|
|
if (!updated) throw new NativeStatusRaceError();
|
|
} else {
|
|
failAt("status_projection", input.failpoint);
|
|
const projected = await issueService(tx as unknown as Db).update(
|
|
input.issueId,
|
|
{
|
|
status: input.decision.toStatus,
|
|
statusVersion: input.priorStatusVersion + 1,
|
|
lastStatusDecisionId: decisionRow.id,
|
|
unblockDescriptor: input.decision.unblockDescriptor,
|
|
actorAgentId: null,
|
|
actorUserId: null,
|
|
},
|
|
tx,
|
|
publications,
|
|
);
|
|
if (!projected) throw new NativeStatusRaceError();
|
|
updated = projected;
|
|
if (updated.originKind === "routine_execution") {
|
|
await syncRoutineRunStatusForIssue(tx as unknown as Db, input.issueId);
|
|
}
|
|
materialized.unshift({
|
|
effectKind: "issue_status_projection",
|
|
targetType: "issue",
|
|
targetId: input.issueId,
|
|
payload: {
|
|
fromStatus: issue.status,
|
|
toStatus: input.decision.toStatus,
|
|
reasonCode,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (input.decision.statusAction === "done" && issue.status !== "done") {
|
|
const issueSvc = issueService(tx as unknown as Db);
|
|
const dependents = await issueSvc.listWakeableBlockedDependents(
|
|
input.issueId,
|
|
);
|
|
const completedResultSummary = await tx
|
|
.select({ resultJson: nativeRunResults.resultJson })
|
|
.from(nativeRunResults)
|
|
.where(
|
|
and(
|
|
eq(nativeRunResults.companyId, input.companyId),
|
|
eq(nativeRunResults.runId, input.runId),
|
|
),
|
|
)
|
|
.limit(1)
|
|
.then((rows) => {
|
|
const summary = record(record(rows[0]?.resultJson).result).summary;
|
|
return typeof summary === "string" && summary.trim().length > 0
|
|
? summary.trim()
|
|
: null;
|
|
});
|
|
const parent = issue.parentId
|
|
? await issueSvc.getWakeableParentAfterChildCompletion(issue.parentId, {
|
|
issueId: input.issueId,
|
|
summary: completedResultSummary,
|
|
})
|
|
: null;
|
|
const parentIsDependent = parent
|
|
? dependents.some((dependent) => dependent.id === parent.id)
|
|
: false;
|
|
// Ordinary children can finish new work after reopening. Key that wake to
|
|
// the committed decision; replaying the decision still shares its identity.
|
|
// Watchdog children retain their stable key to avoid feedback loops.
|
|
const childCompletionDecisionId = issue.originKind === "task_watchdog" ? null : decisionRow.id;
|
|
const childCompletionRevision = childCompletionDecisionId ? `:${childCompletionDecisionId}` : "";
|
|
// The dispatcher must retain this new input until a fresh parent turn can
|
|
// read it. Watchdog handoffs keep their existing coalescing semantics.
|
|
const childCompletionDelivery = childCompletionDecisionId
|
|
? { nativeChildCompletionDecisionId: childCompletionDecisionId }
|
|
: {};
|
|
for (const dependent of dependents) {
|
|
const isCompletedChildParent = parent?.id === dependent.id;
|
|
const idempotencyKey = isCompletedChildParent
|
|
? `issue_children_completed:${dependent.id}:${input.issueId}${childCompletionRevision}`
|
|
: buildIssueBlockersResolvedWakeIdempotencyKey({
|
|
dependentIssueId: dependent.id,
|
|
resolvedBlockerIssueId: input.issueId,
|
|
});
|
|
const childCompletionContext =
|
|
isCompletedChildParent && parent
|
|
? {
|
|
completedChildIssueId: input.issueId,
|
|
...childCompletionDelivery,
|
|
childIssueIds: parent.childIssueIds,
|
|
childIssueSummaries: parent.childIssueSummaries,
|
|
onboardingCompletion: parent.onboardingCompletion,
|
|
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
}
|
|
: null;
|
|
const wakeId = await enqueueWake({
|
|
tx: tx as unknown as Db,
|
|
companyId: input.companyId,
|
|
issueId: dependent.id,
|
|
agentId: dependent.assigneeAgentId,
|
|
reason: isCompletedChildParent
|
|
? "issue_children_completed"
|
|
: "issue_blockers_resolved",
|
|
idempotencyKey,
|
|
payload: {
|
|
resolvedBlockerIssueId: input.issueId,
|
|
blockerIssueIds: dependent.blockerIssueIds,
|
|
...(childCompletionContext ?? {}),
|
|
},
|
|
contextSnapshot: childCompletionContext ?? undefined,
|
|
});
|
|
materialized.push({
|
|
effectKind: isCompletedChildParent
|
|
? "parent_dependency_wake"
|
|
: "dependency_wake",
|
|
targetType: "agent_wakeup_request",
|
|
targetId: wakeId,
|
|
payload: {
|
|
dependentIssueId: dependent.id,
|
|
resolvedBlockerIssueId: input.issueId,
|
|
...(childCompletionContext ?? {}),
|
|
},
|
|
});
|
|
}
|
|
if (parent && !parentIsDependent) {
|
|
const wakeId = await enqueueWake({
|
|
tx: tx as unknown as Db,
|
|
companyId: input.companyId,
|
|
issueId: parent.id,
|
|
agentId: parent.assigneeAgentId,
|
|
reason: "issue_children_completed",
|
|
idempotencyKey: `issue_children_completed:${parent.id}:${input.issueId}${childCompletionRevision}`,
|
|
payload: {
|
|
completedChildIssueId: input.issueId,
|
|
...childCompletionDelivery,
|
|
childIssueIds: parent.childIssueIds,
|
|
childIssueSummaries: parent.childIssueSummaries,
|
|
onboardingCompletion: parent.onboardingCompletion,
|
|
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
},
|
|
contextSnapshot: {
|
|
completedChildIssueId: input.issueId,
|
|
...childCompletionDelivery,
|
|
childIssueIds: parent.childIssueIds,
|
|
childIssueSummaries: parent.childIssueSummaries,
|
|
onboardingCompletion: parent.onboardingCompletion,
|
|
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
},
|
|
});
|
|
materialized.push({
|
|
effectKind: "parent_wake",
|
|
targetType: "agent_wakeup_request",
|
|
targetId: wakeId,
|
|
payload: {
|
|
parentIssueId: parent.id,
|
|
completedChildIssueId: input.issueId,
|
|
...childCompletionDelivery,
|
|
childIssueSummaries: parent.childIssueSummaries,
|
|
onboardingCompletion: parent.onboardingCompletion,
|
|
childIssueSummaryTruncated: parent.childIssueSummaryTruncated,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const [index, effect] of materialized.entries()) {
|
|
await tx
|
|
.insert(statusDecisionEffects)
|
|
.values({
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
decisionId: decisionRow.id,
|
|
ordinal: index + 1,
|
|
effectKind: effect.effectKind,
|
|
targetType: effect.targetType,
|
|
targetId: effect.targetId,
|
|
idempotencyKey: `native-status:${decisionRow.id}:${index + 1}`,
|
|
payload: effect.payload,
|
|
deliveryState: "delivered",
|
|
attemptCount: 1,
|
|
deliveredAt: new Date(),
|
|
})
|
|
.onConflictDoNothing();
|
|
}
|
|
const shadowOnly = input.decision.effects.some(
|
|
(effect) => effect.kind === "record_shadow_decision",
|
|
);
|
|
const applicationState = shadowOnly ? "proposed" : "applied";
|
|
await tx
|
|
.update(statusDecisions)
|
|
.set({
|
|
applicationState,
|
|
appliedAt: shadowOnly ? null : new Date(),
|
|
})
|
|
.where(eq(statusDecisions.id, decisionRow.id));
|
|
await recordCoordinatorDecision(decisionRow.id);
|
|
if (externalChatReviewPresentation && input.reviewResponsePresentation) {
|
|
await restoreNativeChatReviewPresentationInTransaction(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: input.companyId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
decisionId: decisionRow.id,
|
|
resultId: input.reviewResponsePresentation.resultId,
|
|
assessmentId: input.assessmentId,
|
|
},
|
|
);
|
|
}
|
|
const { publication } = await persistActivity(tx as unknown as Db, {
|
|
companyId: input.companyId,
|
|
actorType: "system",
|
|
actorId: "native-status-committer",
|
|
action:
|
|
input.decision.statusAction === "preserve"
|
|
? "issue.status_decision_recorded"
|
|
: "issue.updated",
|
|
entityType: "issue",
|
|
entityId: input.issueId,
|
|
issueId: input.issueId,
|
|
runId: input.runId,
|
|
details: {
|
|
source: "native_status_decision",
|
|
assessmentId: input.assessmentId,
|
|
decisionId: decisionRow.id,
|
|
fromStatus: issue.status,
|
|
toStatus: input.decision.toStatus,
|
|
reasonCode,
|
|
effectCount: materialized.length,
|
|
},
|
|
});
|
|
publications.push(publication);
|
|
return {
|
|
decision: { ...decisionRow, applicationState },
|
|
issue: updated,
|
|
replayed: false,
|
|
};
|
|
});
|
|
|
|
for (const publication of publications) publishActivity(publication);
|
|
for (const terminalRun of terminalRunsToEmit)
|
|
await emitAgentTaskRun(input.db, terminalRun);
|
|
return committed;
|
|
}
|