Files
PaperClipAI/server/src/services/native-runtime/status-decision-committer.ts
T
DottaandPaperclip 8cfedd7df8 fix(runner): restore task monitors and durable timed waits (#15446)
## Thinking Path

> - Paperclip manages AI agents and their task execution.
> - Agents need a durable way to return to work after a delayed check.
> - The issue monitor scheduler already provides a one-shot wake for an
assignee.
> - Native runners reject generic execution-policy writes and had no
bound monitor tool.
> - Scheduling alone is insufficient because native completion also
needs to accept a timed wait.
> - This pull request adds an authorized monitor tool and connects it to
completion and the existing scheduler.
> - An agent can now schedule its next check, end the run, and resume on
the same task.

## Linked Issues or Issue Description

**What happened?**

A native runner could not set its own task monitor. `call_api` correctly
rejected execution-policy writes, while `schedule_wake` had no
production binding. `paperclip_finish` also rejected monitor waits.

**Expected behavior**

A standard native run can set a one-shot monitor on its current task or
another accessible task assigned to the same agent. After a confirmed
schedule on the current task, it can yield. The scheduler later delivers
`issue_monitor_due`.

**Steps to reproduce**

1. Start a standard native task.
2. Ask the agent to check the task again later and end its current run.
3. Inspect available tools and try the generic issue execution-policy
update.
4. Observe the missing native tool and the lifecycle-write denial.

Related PRs: #14680 concerns monitor notes in the shared wake prompt.
#11919 changes attempt-limit scope. This PR adds native scheduling and
completion authority and retains the existing cumulative attempt bounds.
It does not depend on either PR.

## What Changed

- Add provider-neutral `set_task_monitor` with a default current-task
target, future timestamp, required notes, existing bounds, and explicit
clearing.
- Check company, task visibility, ownership, runtime permissions, work
mode, and active-run authority. Preserve review-only restrictions.
Reject the reserved server-owned quota-recovery name before saving or
accepting a native wait.
- Commit the monitor, audit event, and retry receipt together. Retry
receipts survive a successor run without re-arming cleared or consumed
timers.
- Permit `paperclip_finish` to yield to a persisted monitor. Recheck
ownership and the schedule when committing final disposition. Release
execution without an immediate continuation.
- Preserve due monitors during native execution. Fence wake admission
and consumption against replacement, clearing, reassignment, and
completion. Preserve unrelated review policy.
- Expose scheduled and consumed monitor instructions in task context.
Update provider schemas, Rust validation, generated contracts, and
execution documentation.
- Add an opt-in live Codex smoke script with isolated data and explicit
run/session/runner/process evidence.

## Verification

- Repository `pnpm -r typecheck` and `pnpm build` passed after rebase.
Server typecheck passed again after review fixes. All CI test shards
pass on `3def77b1b`, including runner TypeScript/Rust, server,
serialized server, workspace, and browser tests. All CI gates are green,
including the canary dry run. Greptile is 5/5 on the same commit with
zero unresolved threads.
- The local monolithic `pnpm test:run`, started before the rebase, was
interrupted after current-head CI test coverage passed. It is not
counted as a standalone full-suite pass; the focused local regression
suites passed.
- Targeted server tests cover scheduling, replacement, clearing, policy
preservation, cumulative bounds, cross-run retries, permissions,
provider-neutral discovery, review restrictions, completion authority,
and scheduler/finalizer races.
- Runner contract/catalog/semantic tests and Rust terminal-tool tests
cover the new operation and monitor completion.
- Live Codex test passed twice (latest live run on `e0bcd63e6`) in a
temporary database and workspace, with a 300,000 ms warm window. First
run `67bd7709-c089-4d4a-9d2b-0d6b618a34b0` yielded at
`2026-10-07T13:15:03.274Z`. Second run
`97c323f9-595a-4cc5-a007-db5a2fbb937c` started at `13:15:30.952Z`,
received `issue_monitor_due`, and completed the same task. Exactly one
monitor wake was recorded.
- Both live runs used native session
`7b1dd753-1c9b-4e7a-b22f-a125dbc3748c`, runner
`e90d9a1b-3502-4ee3-b15e-edc024c555d4`, provider session
`01a11680-6d01-70c0-9a55-db7246ed66c3`, and PID `64218` with the same
process start time. This proves warm reuse for that local Codex test,
not only successful scheduling.
- Reproduce the paid live test with `node --import
./server/node_modules/tsx/dist/loader.mjs
server/scripts/smoke-native-task-monitor.ts --run`, with the installed
Codex binary on `PATH` and a valid local login.

## Risks

- The scheduler now defers monitor dispatch while the task has an active
native run. A stuck run still depends on the existing recovery
lifecycle.
- Idempotency uses the existing run ledger; no table or migration is
added.
- Other providers share the tested tool and completion contracts. Only
Codex received a live model test.
- Existing `call_api` lifecycle restrictions remain enforced. Monitor
waits do not bypass task blockers, reviews, or approvals.

## Model Used

OpenAI Codex, GPT-6 family, with tool use, code execution, and
TypeScript/Rust editing. The session does not expose the exact deployed
model ID or context-window size.

## 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
- [x] All Paperclip CI gates are green
- [x] 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>
2026-10-07 08:41:24 -05:00

2248 lines
76 KiB
TypeScript

import { eligibleIssueMonitorWait } from "../issue-monitors.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 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 {
isExternalChatWaitAuthorizationContention,
resolveExternalChatResponseWaitAuthorizationInTransaction,
} from "./chat-attachment-reuse.js";
import { issueService } from "../issues.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";
export class NativeStatusRaceError extends Error {
readonly code = "native_status_race" as const;
constructor() {
super("native_status_race");
this.name = "NativeStatusRaceError";
}
}
/** 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;
}) {
return input.db.transaction(async (tx) => {
const coordinator = await tx
.select({
decisionId: nativeRunFinalizations.decisionId,
})
.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");
}
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 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),
),
);
}
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 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") {
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;
requireExternalChatResponseWaitAuthorization?: { agentId: string };
requirePlanWaitSource?: NativePlanWaitSource;
requireMonitorWait?: { agentId: string; nextCheckAt: string };
requireNoPendingChildCompletion?: NativeChildCompletionRecipient;
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 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();
// 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();
}
if (reasonCode === "scheduled_monitor_waiting") {
const expected = input.requireMonitorWait;
if (!expected || issue.workMode !== "standard" || issue.executionRunId !== input.runId ||
input.decision.statusAction !== "preserve" || input.decision.effects.length !== 1 ||
input.decision.effects[0]?.kind !== "release_checkout" ||
eligibleIssueMonitorWait(issue, expected.agentId) !== expected.nextCheckAt) {
throw new NativeStatusRaceError();
}
}
// 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();
}
}
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 tx
.update(nativeRunFinalizations)
.set({
phase: finalizationError ? "retryable_failure" : "committed",
assessmentId: input.assessmentId,
decisionId,
leaseOwner: null,
leaseExpiresAt: 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 } : {}),
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 };
}
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");
}
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;
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;
}