Files
PaperClipAI/server/src/services/native-runtime/native-interaction-bridge.ts
T
Devin FoleyandPaperclip 5847f047c7 fix(connections): retire requests for already available tools (#15856)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Connection cards let users provide the access an agent needs.
> - Users can complete setup in Apps while an earlier card stays
pending.
> - A stale card can then cancel recovery despite valid existing access.
> - This change verifies existing access and expires the obsolete
request.
> - It records a system outcome and sends one durable continuation.
> - Approval, identity, permission, and workspace recovery gates remain
in place.

## Linked Issues or Issue Description

**What happened?**

An ordinary tool connection request stayed pending after the addressed
user connected the service in Apps. A later conversation retry was
cancelled with `issue_waiting_for_response`.

**Expected behavior**

A request expires when the same user and requesting agent already have
usable access. The agent gets a durable tool refresh. The system records
no new human approval.

**Steps to reproduce**

1. Have an agent request a tool connection on an active task.
2. Connect the service through Apps with the addressed user's existing
grant, agent install, and allowed tools.
3. Let a conversation run stop and retry. The pending card still blocks
the retry on the base branch.

Related: #12345 introduced the connection request flow. This fix uses
its existing access checks and delivery queue.

## What Changed

- Verify company, task ownership, user identity, membership, health,
grants, installs, and task tool policy before expiring an ordinary
request.
- Preserve AI account, channel, additional permission, and upstream
provider requests on their existing paths.
- Save the system outcome, audit event, and continuation in one
transaction. Notify an idle delivery worker when a request commits.
- Recheck access and other questions or approvals before delivery,
retry, final dispatch, and native projection. Refresh system
observations for an authorized replacement account.
- Rearm delivery when access changes before provider work starts. Verify
the host's admission receipt against execution evidence before allowing
retry without a repair incident.
- Require the current delivery claim when marking an attempt complete.
An old completion cannot overwrite a rearmed continuation.
- Add embedded-Postgres regressions for permissions, policy,
concurrency, restart, replacement accounts, both dispatch gates, and the
delivery race. Keep genuine workspace repair holds blocking.

## Verification

- Latest-head connection, wake-context, dispatch, and legacy recovery
suites: 124 tests passed. The connection suite includes 44 real-Postgres
cases.
- Server typecheck passed. Full `pnpm -r typecheck` and `pnpm build`
passed on the latest head.
- `pnpm test:run` was run locally. Its general-server group passed
18,583 tests and failed 20. That long run started before the review
fixes, then loaded new test cases against modules transformed before
those fixes. The affected latest-head suites pass in a fresh process.
- One full-run failure also reproduces on unchanged base commit
de9ab8e7e0: native-session-resume's damaged-prior-epoch replacement
case. It is outside this change. Full-run groups after general-server
did not run because that command stopped on failure.
- Apex reviewed commit 96b96297f4 at 5/5. All review threads are
resolved. All latest-head CI gates passed. The branch has no merge
conflicts.

## Risks

A false readiness result could retire a request too early. The fix
reuses the runtime access selection rules, makes no new grants, and
rechecks access at delivery. Ordinary expired requests do not authorize
continuation. A failed workspace restore still requires repair through
the existing gate. No schema migration is needed.

## Model Used

OpenAI GPT-6 through Codex, with repository inspection, code editing,
shell execution, and test tools. The session identifies the model family
as GPT-6. It does not expose a more specific runtime 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 (focused suites; full local
limitations are listed above)
- [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-11 09:30:45 -07:00

958 lines
41 KiB
TypeScript

import type { Db } from "@paperclipai/db";
import type { NativeInteractionResponseEnvelope } from "../../vendor/paperclip-runner/index.js";
import { and, asc, eq, inArray } from "drizzle-orm";
import {
agents,
toolActionRequests,
toolInvocations,
heartbeatRuns,
issues,
issueThreadInteractions,
nativeRunFinalizations,
nativeRunResults,
statusDecisions,
workAssessments,
} from "@paperclipai/db";
import { getAgentWorkEligibility } from "@paperclipai/shared";
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
import { commitNativeStatusDecision } from "./status-decision-committer.js";
import { nativeSha256 } from "./canonical.js";
import { recordNativeAttentionAssessment } from "./work-assessments.js";
import { formatDurableQuestionResponseSummary } from "../question-response-delivery.js";
import { findSatisfiedToolConnection } from "../satisfied-connection-intents.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
type NativeGovernanceGate,
type NativeStatusDecision,
type NativeStatusEffect,
} from "./status-arbiter.js";
export class NativeInteractionBridgeError extends Error {
constructor(readonly code: string, message: string) {
super(message);
this.name = "NativeInteractionBridgeError";
}
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
export type NativeAttentionMaterializedTarget = {
effectKind: string;
targetType: string;
targetId: string;
};
/**
* Applies audit-only attention outcomes that intentionally create no status
* decision. The durable interaction/coordinator mutation is the proof; a
* synthetic delivered effect row is neither created nor accepted.
*/
export async function applyNativeAttentionStatusDecision(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
decision: NativeStatusDecision;
targetInteractionId?: string;
canonicalRequestId?: string;
}): Promise<NativeAttentionMaterializedTarget[]> {
if (input.decision.statusAction !== "preserve") {
throw new NativeInteractionBridgeError("native_attention_status_authority_invalid", "Audit-only attention cannot change issue status");
}
return input.db.transaction(async (tx) => {
const targets: NativeAttentionMaterializedTarget[] = [];
for (const effect of input.decision.effects) {
if (effect.kind === "link_canonical_request") {
const boundIds = [input.canonicalRequestId, input.targetInteractionId]
.filter((id): id is string => typeof id === "string");
const interactions = await tx.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
})
.from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.companyId, input.companyId),
eq(issueThreadInteractions.issueId, input.issueId),
...(boundIds.length > 0 ? [inArray(issueThreadInteractions.id, boundIds)] : []),
)).limit(2);
const canonical = input.canonicalRequestId
? interactions.find((interaction) => interaction.id === input.canonicalRequestId)
: interactions[0];
const duplicate = input.targetInteractionId
? interactions.find((interaction) => interaction.id === input.targetInteractionId)
: interactions.find((interaction) => interaction.id !== canonical?.id);
if (!canonical || !duplicate || canonical.id === duplicate.id) {
throw new NativeInteractionBridgeError("native_attention_canonical_pair_missing", "Canonical and duplicate requests are required");
}
const summary = `Canonical native attention request: ${canonical.id}`;
if (duplicate.summary !== summary) {
await tx.update(issueThreadInteractions).set({
summary,
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, duplicate.id));
}
targets.push({ effectKind: effect.kind, targetType: "issue_thread_interaction", targetId: duplicate.id });
continue;
}
if (effect.kind === "record_stale_response") {
const interaction = await tx.select({
id: issueThreadInteractions.id,
summary: issueThreadInteractions.summary,
})
.from(issueThreadInteractions).where(and(
eq(issueThreadInteractions.companyId, input.companyId),
eq(issueThreadInteractions.issueId, input.issueId),
...(input.targetInteractionId ? [eq(issueThreadInteractions.id, input.targetInteractionId)] : []),
)).limit(1).then((rows) => rows[0] ?? null);
if (!interaction) throw new NativeInteractionBridgeError("native_stale_response_missing", "Stale response is not durable");
const summary = "Response retained for audit after native supersession.";
if (interaction.summary !== summary) {
await tx.update(issueThreadInteractions).set({
summary,
updatedAt: new Date(),
}).where(eq(issueThreadInteractions.id, interaction.id));
}
targets.push({ effectKind: effect.kind, targetType: "issue_thread_interaction", targetId: interaction.id });
continue;
}
if (effect.kind === "record_finalization_error") {
const [coordinator] = await tx.update(nativeRunFinalizations).set({
phase: "terminal_failure",
failureCode: effect.cause,
failureDetail: { nextAction: effect.nextAction, recoveryOwner: { kind: "agent", agentId: effect.agentId } },
nextAttemptAt: null,
updatedAt: new Date(),
}).where(and(
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.companyId, input.companyId),
eq(nativeRunFinalizations.issueId, input.issueId),
)).returning({ runId: nativeRunFinalizations.runId });
if (!coordinator) throw new NativeInteractionBridgeError("native_finalization_coordinator_missing", "Native coordinator is required");
await tx.update(heartbeatRuns).set({
nativePhase: "terminal_failure",
nativePhaseUpdatedAt: new Date(),
errorCode: effect.cause,
updatedAt: new Date(),
}).where(and(eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.companyId, input.companyId)));
targets.push({ effectKind: effect.kind, targetType: "native_run_finalization", targetId: coordinator.runId });
continue;
}
throw new NativeInteractionBridgeError("native_attention_effect_unimplemented", effect.kind);
}
return targets;
});
}
/**
* Materialize only interactions that authorized this run's wake. Resolution
* remains owned by the existing interaction service; this bridge is a
* read-only, identity-bound projection and never receives credentials.
*/
export async function materializeNativeInteractionResponses(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
agentId: string;
interactionIds: string[];
}): Promise<NativeInteractionResponseEnvelope[]> {
const requestedIds = new Set(input.interactionIds.filter(Boolean));
if (requestedIds.size === 0) return [];
const [interactions, issue] = await Promise.all([
issueThreadInteractionService(input.db).listForIssue(input.issueId),
input.db.select().from(issues).where(and(
eq(issues.id, input.issueId),
eq(issues.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null),
]);
if (!issue) throw new NativeInteractionBridgeError("native_interaction_binding_mismatch", "Issue binding not found");
const responses: NativeInteractionResponseEnvelope[] = [];
for (const interaction of interactions) {
if (!requestedIds.has(interaction.id)) continue;
if (interaction.companyId !== input.companyId || interaction.issueId !== input.issueId) {
throw new NativeInteractionBridgeError(
"native_interaction_binding_mismatch",
`Interaction ${interaction.id} is not bound to the native company and issue`,
);
}
if (interaction.kind === "request_confirmation" && interaction.payload.toolAction) {
const action = interaction.payload.toolAction;
if (["accepted", "rejected"].includes(interaction.status) && (interaction.resolvedByAgentId || interaction.resolvedByRunId === input.runId)) {
throw new NativeInteractionBridgeError("native_interaction_self_approval", "Agents cannot resolve governed tool reviews");
}
const [request] = await input.db.select().from(toolActionRequests).where(and(eq(toolActionRequests.id, action.actionRequestId), eq(toolActionRequests.companyId, input.companyId), eq(toolActionRequests.issueId, input.issueId), eq(toolActionRequests.interactionId, interaction.id), eq(toolActionRequests.invocationId, action.invocationId)));
const [invocation] = await input.db.select().from(toolInvocations).where(and(eq(toolInvocations.id, action.invocationId), eq(toolInvocations.companyId, input.companyId), eq(toolInvocations.issueId, input.issueId), eq(toolInvocations.agentId, input.agentId)));
if (!request || !invocation || request.requestedByAgentId !== input.agentId || request.canonicalArgumentsHash !== action.argumentsHash) {
throw new NativeInteractionBridgeError("native_interaction_governed_request_unresolved", "Tool review has no matching authoritative invocation");
}
if (["expired", "cancelled"].includes(request.status)) {
if (interaction.status !== request.status && !(interaction.status === "accepted" && interaction.result?.toolAction?.status === "expired")) throw new NativeInteractionBridgeError("native_interaction_governed_result_mismatch", "Tool review lifecycle does not match its request");
responses.push({ interactionId: interaction.id, kind: interaction.kind, response: { status: interaction.status, result: structuredClone(interaction.result), executionStatus: request.status } });
continue;
}
if (!request.decidedByUserId || request.decidedByUserId !== interaction.resolvedByUserId || !["executed", "failed", "rejected"].includes(request.status) || (request.status === "rejected" ? interaction.status !== "rejected" : interaction.status !== "accepted")) {
throw new NativeInteractionBridgeError("native_interaction_governed_request_unresolved", "Tool review must have a human decision and an authoritative terminal execution outcome");
}
const expectedInvocationStatus = request.status === "executed" ? "succeeded" : request.status === "rejected" ? "denied" : "failed";
if (invocation.status !== expectedInvocationStatus || (request.status !== "rejected" && interaction.result?.toolAction?.status !== request.status)) throw new NativeInteractionBridgeError("native_interaction_governed_result_mismatch", "Tool review outcome does not match its invocation");
}
const interactionResult = record(interaction.result);
if (interaction.kind === "connection_intent" && interaction.status === "expired"
&& interactionResult.outcome === "expired" && typeof interactionResult.connectionId === "string") {
const [run] = await input.db.select({ responsibleUserId: heartbeatRuns.responsibleUserId }).from(heartbeatRuns)
.where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.agentId, input.agentId)));
const ready = issue.assigneeAgentId === input.agentId && run?.responsibleUserId === interaction.addresseeUserId
? await findSatisfiedToolConnection(input.db, issue, { ...interaction, addresseeUserId: interaction.addresseeUserId ?? null, status: "pending" }, input.runId) : null;
if (!ready || ready.id !== interactionResult.connectionId) {
throw new NativeInteractionBridgeError("native_interaction_unresolved", "The existing connection is no longer available");
}
responses.push({ interactionId: interaction.id, kind: interaction.kind,
response: { status: "expired", result: structuredClone(interaction.result) as unknown as Record<string, unknown> } });
continue;
}
const supersessionOutcome = interaction.status === "expired"
&& ["superseded_by_newer_request", "superseded_by_comment", "stale_target"].includes(String(interactionResult.outcome));
if (supersessionOutcome) {
const duplicate = interactionResult.outcome === "superseded_by_newer_request";
const decision = resolveNativeAttentionStatus({
facts: duplicate
? {
companyScopeValid: true,
responseState: "none",
route: "duplicate",
summary: interaction.summary ?? interaction.title ?? "Duplicate native attention request",
}
: {
companyScopeValid: true,
responseState: "stale",
route: "context",
summary: interaction.summary ?? interaction.title ?? "Stale native attention response",
},
priorIssueStatus: issue.status as NativeAuthoritativeIssueStatus,
agentId: input.agentId,
});
await applyNativeAttentionStatusDecision({
db: input.db,
companyId: input.companyId,
issueId: input.issueId,
runId: input.runId,
decision,
targetInteractionId: interaction.id,
canonicalRequestId: typeof interactionResult.supersededByInteractionId === "string"
? interactionResult.supersededByInteractionId
: undefined,
});
continue;
}
if (
interaction.resolvedByRunId === input.runId
|| (
interaction.resolvedByAgentId === input.agentId
&& interaction.createdByAgentId === input.agentId
)
) {
throw new NativeInteractionBridgeError(
"native_interaction_self_approval",
`Run ${input.runId} cannot consume a response it resolved itself`,
);
}
const hasInteractionResolver = Boolean(interaction.resolvedByUserId || interaction.resolvedByAgentId);
if (interaction.kind !== "request_item_verdicts" && !hasInteractionResolver) {
throw new NativeInteractionBridgeError(
"native_interaction_unresolved",
`Interaction ${interaction.id} has no authorized resolver`,
);
}
if (interaction.kind === "request_confirmation" || interaction.kind === "request_checkbox_confirmation") {
if (interaction.resolvedByAgentId === input.agentId) {
throw new NativeInteractionBridgeError(
"native_interaction_self_approval",
`Agent ${input.agentId} cannot consume a confirmation it resolved`,
);
}
if (
(interaction.status !== "accepted" && interaction.status !== "rejected")
|| !interaction.result
|| (interaction.result.outcome !== "accepted" && interaction.result.outcome !== "rejected")
) {
throw new NativeInteractionBridgeError(
"native_interaction_unresolved",
`Confirmation ${interaction.id} is not authoritatively resolved`,
);
}
const resolution = resolveNativeAttentionStatus({
facts: {
companyScopeValid: true,
responseState: "resolved",
route: "context",
summary: interaction.summary ?? interaction.title ?? "Resolved native interaction",
},
priorIssueStatus: issue.status as NativeAuthoritativeIssueStatus,
agentId: input.agentId,
});
if (resolution.reasonCode !== "attention_resolved_from_context") {
throw new NativeInteractionBridgeError("native_attention_resolution_invalid", "Resolved interaction has no continuation policy");
}
responses.push({
interactionId: interaction.id,
kind: interaction.kind,
response: {
status: interaction.status,
result: structuredClone(interaction.result) as unknown as Record<string, unknown>,
},
});
continue;
}
if (interaction.kind === "suggest_tasks" || interaction.kind === "connection_intent") {
if (
(interaction.status !== "accepted" && interaction.status !== "rejected")
|| !interaction.result
) {
throw new NativeInteractionBridgeError(
"native_interaction_unresolved",
`Suggested-task interaction ${interaction.id} is not authoritatively resolved`,
);
}
responses.push({
interactionId: interaction.id,
kind: interaction.kind,
response: {
status: interaction.status,
result: structuredClone(interaction.result) as unknown as Record<string, unknown>,
},
});
continue;
}
if (interaction.kind === "ask_user_questions") {
if (interaction.status !== "answered" || !interaction.result || interaction.result.cancelled === true) {
throw new NativeInteractionBridgeError(
"native_interaction_unresolved",
`Question interaction ${interaction.id} is not authoritatively answered`,
);
}
const resolution = resolveNativeAttentionStatus({
facts: {
companyScopeValid: true,
responseState: "resolved",
route: "context",
summary: interaction.summary ?? interaction.title ?? "Answered native interaction",
},
priorIssueStatus: issue.status as NativeAuthoritativeIssueStatus,
agentId: input.agentId,
});
if (resolution.reasonCode !== "attention_resolved_from_context") {
throw new NativeInteractionBridgeError("native_attention_resolution_invalid", "Answered interaction has no continuation policy");
}
const result = structuredClone(interaction.result) as unknown as Record<string, unknown>;
const summaryMarkdown = formatDurableQuestionResponseSummary(interaction);
responses.push({
interactionId: interaction.id,
kind: interaction.kind,
response: {
status: interaction.status,
result: summaryMarkdown
? { ...result, summaryMarkdown }
: result,
},
});
continue;
}
if (interaction.kind === "request_item_verdicts") {
const result = record(interaction.result);
const items = Array.isArray(result.items) ? result.items.map(record) : [];
const complete = result.complete === true;
const hasAuthorizedItemResolver = items.some((item) =>
typeof item.resolvedByUserId === "string"
|| (typeof item.resolvedByAgentId === "string" && item.resolvedByAgentId !== input.agentId));
const hasSelfResolvedItem = items.some((item) => item.resolvedByAgentId === input.agentId);
if (hasSelfResolvedItem) {
throw new NativeInteractionBridgeError(
"native_interaction_self_approval",
`Agent ${input.agentId} cannot consume item verdicts it resolved`,
);
}
if (
!interaction.result
|| (interaction.status !== "pending" && interaction.status !== "answered")
|| (interaction.status === "answered" && !complete)
|| items.length === 0
|| !hasAuthorizedItemResolver
) {
throw new NativeInteractionBridgeError(
"native_interaction_unresolved",
`Item-verdict interaction ${interaction.id} has no authoritative response`,
);
}
responses.push({
interactionId: interaction.id,
kind: interaction.kind,
response: {
status: interaction.status,
result: structuredClone(interaction.result) as unknown as Record<string, unknown>,
},
});
continue;
}
throw new NativeInteractionBridgeError(
"native_runtime_request_unsupported",
"Interaction kind is not supported by the native input contract",
);
}
const missing = [...requestedIds].filter((id) => !responses.some((response) => response.interactionId === id));
if (missing.length > 0) {
throw new NativeInteractionBridgeError(
"native_interaction_missing",
`Native interaction response not found: ${missing.join(", ")}`,
);
}
return responses.sort((left, right) => left.interactionId.localeCompare(right.interactionId));
}
export type LegacyQuestionResponseWakeProjection = {
interactionId: string;
summaryMarkdown: string;
};
/**
* Legacy adapters do not consume the runner's closed interaction-response
* envelope. Project an answered question set into their wake prompt at invoke
* time instead. The caller must keep this projection ephemeral so the
* interaction result remains the only control-plane copy of the answers.
*/
export async function materializeLegacyQuestionResponseWakeProjection(input: {
db: Db;
companyId: string;
issueId: string;
runId: string;
agentId: string;
interactionId: string;
}): Promise<LegacyQuestionResponseWakeProjection | null> {
const responses = await materializeNativeInteractionResponses({
...input,
interactionIds: [input.interactionId],
});
const response = responses.find((candidate) =>
candidate.interactionId === input.interactionId
&& candidate.kind === "ask_user_questions"
&& candidate.response.status === "answered"
);
if (!response) return null;
const summaryMarkdown = record(response.response.result).summaryMarkdown;
if (typeof summaryMarkdown !== "string" || summaryMarkdown.trim().length === 0) return null;
return {
interactionId: response.interactionId,
summaryMarkdown: summaryMarkdown.trim(),
};
}
/** Native runtimes never receive credentials and never auto-approve requests. */
export function rejectUnsupportedNativeRuntimeRequest(requestKind: string) {
return {
accepted: false as const,
code: "native_runtime_request_unsupported" as const,
requestKind,
credentialsInjected: false as const,
selfApproval: false as const,
};
}
export type NativeAttentionFacts = {
companyScopeValid: boolean;
responseState: "none" | "resolved" | "stale" | "expired";
route: "alternate_track" | "context" | "retry" | "agent" | "human" | "duplicate" | "recovery";
summary: string;
budgetExhausted?: boolean;
governanceGate?: NativeGovernanceGate | null;
resolvedTargetAgentId?: string | null;
};
export type NativeAttentionCandidate = {
route: NativeAttentionFacts["route"];
summary: string;
responseState?: NativeAttentionFacts["responseState"];
targetAgentId?: string | null;
targetCompanyId?: string | null;
budgetExhausted?: boolean;
governanceGate?: NativeGovernanceGate | null;
targetInteractionId?: string;
canonicalRequestId?: string;
};
function nonEmptyText(value: unknown) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function candidateFromPersistedRequest(request: Record<string, unknown>): NativeAttentionCandidate {
const summary = nonEmptyText(request.summary);
if (!summary) {
throw new NativeInteractionBridgeError(
"native_attention_request_invalid",
"Persisted native attention requires a summary",
);
}
const target = record(request.target);
const capability = nonEmptyText(request.requestedCapability) ?? nonEmptyText(request.kind);
const ownerClass = nonEmptyText(target.ownerClass);
const requiredAuthority = nonEmptyText(request.requiredAuthority);
const responseState = nonEmptyText(request.responseState);
const governanceGate = record(request.governanceGate);
let route: NativeAttentionCandidate["route"];
if (capability === "context_lookup") route = "context";
else if (capability === "retry") route = "retry";
else if (capability === "duplicate") route = "duplicate";
else if (capability === "alternate_track" || ownerClass === "current_agent") route = "alternate_track";
else if (capability === "domain_expertise" || ownerClass === "agent") route = "agent";
else if (
["review", "credential_grant", "governed_approval", "subjective_decision", "external_action"].includes(capability ?? "")
|| ["board_user", "external_system"].includes(ownerClass ?? "")
|| ["board", "external"].includes(requiredAuthority ?? "")
) route = "human";
else {
throw new NativeInteractionBridgeError(
"native_attention_request_invalid",
`Persisted native attention capability ${capability ?? "unknown"} has no authorized route`,
);
}
return {
route,
summary,
responseState: ["none", "resolved", "stale", "expired"].includes(responseState ?? "")
? responseState as NativeAttentionFacts["responseState"]
: undefined,
targetAgentId: nonEmptyText(target.agentId),
targetCompanyId: nonEmptyText(target.companyId),
budgetExhausted: request.budgetExhausted === true,
governanceGate: governanceGate.kind === "interaction" && nonEmptyText(governanceGate.id)
? { kind: "interaction", id: nonEmptyText(governanceGate.id)! }
: null,
targetInteractionId: nonEmptyText(request.targetInteractionId) ?? undefined,
canonicalRequestId: nonEmptyText(request.canonicalRequestId) ?? undefined,
};
}
export type PersistedNativeAttentionReceipt = {
requestId: string;
route: NativeAttentionCandidate["route"];
assessmentId: string;
decisionId: string | null;
reasonCode: string | null;
resolvedTargetAgentId: string | null;
materializedTargets: NativeAttentionMaterializedTarget[];
replayed: boolean;
};
/**
* Runtime-reachable ingress for accepted attention facts. It starts from the
* immutable package result row, derives an authorized route, appends one
* assessment/decision lineage per request, and lets the existing issue and
* interaction owners materialize the effect.
*/
export async function routePersistedNativeResultAttention(input: {
db: Db;
runId: string;
}): Promise<PersistedNativeAttentionReceipt[]> {
const binding = await input.db.select({
companyId: heartbeatRuns.companyId,
runtimeMode: heartbeatRuns.runtimeMode,
resultId: nativeRunFinalizations.resultId,
issueId: nativeRunFinalizations.issueId,
}).from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.where(eq(heartbeatRuns.id, input.runId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!binding || binding.runtimeMode !== "native" || !binding.resultId) {
throw new NativeInteractionBridgeError(
"native_attention_binding_mismatch",
"Accepted native result binding not found",
);
}
const resultRow = await input.db.select().from(nativeRunResults).where(and(
eq(nativeRunResults.id, binding.resultId),
eq(nativeRunResults.companyId, binding.companyId),
eq(nativeRunResults.issueId, binding.issueId),
eq(nativeRunResults.runId, input.runId),
eq(nativeRunResults.schemaStatus, "accepted"),
)).limit(1).then((rows) => rows[0] ?? null);
if (!resultRow) {
throw new NativeInteractionBridgeError(
"native_attention_result_missing",
"Accepted native result row not found",
);
}
const result = record(record(resultRow.resultJson).result);
const requests = Array.isArray(result.attentionRequests)
? result.attentionRequests.map(record)
: [];
const receipts: PersistedNativeAttentionReceipt[] = [];
for (const [index, request] of requests.entries()) {
const candidate = candidateFromPersistedRequest(request);
const requestId = nonEmptyText(request.id) ?? nativeSha256({
runId: input.runId,
resultId: resultRow.id,
index,
request,
});
const existingAssessment = await input.db.select({ id: workAssessments.id })
.from(workAssessments).where(and(
eq(workAssessments.companyId, binding.companyId),
eq(workAssessments.issueId, binding.issueId),
eq(workAssessments.runId, input.runId),
eq(workAssessments.triggerKind, "native_attention"),
eq(workAssessments.triggerRef, requestId),
)).limit(1).then((rows) => rows[0] ?? null);
if (existingAssessment) {
const existingDecision = await input.db.select({
id: statusDecisions.id,
reasonCode: statusDecisions.reasonCode,
decisionJson: statusDecisions.decisionJson,
}).from(statusDecisions).where(and(
eq(statusDecisions.companyId, binding.companyId),
eq(statusDecisions.assessmentId, existingAssessment.id),
)).limit(1).then((rows) => rows[0] ?? null);
if (existingDecision) {
const decisionJson = record(existingDecision.decisionJson);
const effects = Array.isArray(decisionJson.effects) ? decisionJson.effects.map(record) : [];
const delegation = effects.find((effect) => effect.kind === "create_delegated_issue");
const finalizationError = effects.find((effect) => effect.kind === "record_finalization_error");
await input.db.update(nativeRunFinalizations).set({
phase: finalizationError ? "retryable_failure" : "committed",
assessmentId: existingAssessment.id,
decisionId: existingDecision.id,
leaseOwner: null,
leaseExpiresAt: null,
failureCode: nonEmptyText(finalizationError?.cause),
failureDetail: finalizationError ? {
originalFailureCode: finalizationError.cause,
nextAction: finalizationError.nextAction,
} : null,
nextAttemptAt: finalizationError ? new Date(Date.now() + 30_000) : null,
updatedAt: new Date(),
}).where(and(
eq(nativeRunFinalizations.runId, input.runId),
eq(nativeRunFinalizations.companyId, binding.companyId),
eq(nativeRunFinalizations.issueId, binding.issueId),
));
receipts.push({
requestId,
route: candidate.route,
assessmentId: existingAssessment.id,
decisionId: existingDecision.id,
reasonCode: existingDecision.reasonCode,
resolvedTargetAgentId: nonEmptyText(delegation?.agentId),
materializedTargets: [],
replayed: true,
});
continue;
}
}
const issue = await input.db.select({
status: issues.status,
statusVersion: issues.statusVersion,
lastStatusDecisionId: issues.lastStatusDecisionId,
}).from(issues).where(and(
eq(issues.id, binding.issueId),
eq(issues.companyId, binding.companyId),
)).limit(1).then((rows) => rows[0] ?? null);
if (!issue) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Issue binding not found");
const coordinator = await input.db.select({
phase: nativeRunFinalizations.phase,
assessmentId: nativeRunFinalizations.assessmentId,
decisionId: nativeRunFinalizations.decisionId,
}).from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, input.runId))
.limit(1).then((rows) => rows[0] ?? null);
if (!coordinator) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Coordinator binding not found");
const assessment = await recordNativeAttentionAssessment({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
turnId: resultRow.turnId,
contractId: resultRow.completionContractId,
resultId: resultRow.id,
requestId,
request,
routingFacts: candidate as unknown as Record<string, unknown>,
priorIssueStatus: issue.status,
priorStatusVersion: Number(issue.statusVersion),
priorDecisionId: issue.lastStatusDecisionId,
supersedesAssessmentId: coordinator.assessmentId,
});
const routed = await routeNativeAttention({
db: input.db,
runId: input.runId,
assessmentId: assessment.id,
supersedesCommittedDecisionId: coordinator.phase === "committed"
? coordinator.decisionId ?? undefined
: undefined,
candidate,
});
receipts.push({
requestId,
route: candidate.route,
assessmentId: assessment.id,
decisionId: routed.decisionId,
reasonCode: routed.decision.reasonCode,
resolvedTargetAgentId: routed.resolvedTargetAgentId,
materializedTargets: routed.targets,
replayed: existingAssessment !== null,
});
}
if (receipts.length > 0) {
await input.db.transaction(async (tx) => {
const run = await tx.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns).where(and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, binding.companyId),
)).for("update").limit(1).then((rows) => rows[0] ?? null);
if (!run) throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Heartbeat run missing");
await tx.update(heartbeatRuns).set({
resultJson: { ...record(run.resultJson), nativeAttentionRouting: receipts },
updatedAt: new Date(),
}).where(eq(heartbeatRuns.id, input.runId));
});
}
return receipts;
}
/**
* Internal owner-routing step for native attention. The persisted-result
* ingress above supplies the server-owned assessment and untrusted candidate;
* this step resolves company scope and commits or audit-materializes the
* authority decision.
*/
export async function routeNativeAttention(input: {
db: Db;
runId: string;
assessmentId?: string;
supersedesCommittedDecisionId?: string;
candidate: NativeAttentionCandidate;
}) {
const binding = await input.db.select({
companyId: heartbeatRuns.companyId,
agentId: heartbeatRuns.agentId,
runtimeMode: heartbeatRuns.runtimeMode,
issueId: nativeRunFinalizations.issueId,
assessmentId: nativeRunFinalizations.assessmentId,
issueStatus: issues.status,
issueStatusVersion: issues.statusVersion,
issueDecisionId: issues.lastStatusDecisionId,
}).from(heartbeatRuns)
.innerJoin(nativeRunFinalizations, eq(nativeRunFinalizations.runId, heartbeatRuns.id))
.innerJoin(issues, and(
eq(issues.id, nativeRunFinalizations.issueId),
eq(issues.companyId, heartbeatRuns.companyId),
))
.where(eq(heartbeatRuns.id, input.runId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!binding || binding.runtimeMode !== "native") {
throw new NativeInteractionBridgeError("native_attention_binding_mismatch", "Native attention run binding not found");
}
const companyAgents = await input.db.select({
id: agents.id,
companyId: agents.companyId,
name: agents.name,
status: agents.status,
reportsTo: agents.reportsTo,
}).from(agents).where(eq(agents.companyId, binding.companyId)).orderBy(asc(agents.createdAt), asc(agents.id));
const targetSuggestion = input.candidate.targetAgentId
? await input.db.select({ id: agents.id, companyId: agents.companyId })
.from(agents).where(eq(agents.id, input.candidate.targetAgentId)).limit(1)
.then((rows) => rows[0] ?? null)
: null;
const companyScopeValid = (
!input.candidate.targetCompanyId
|| input.candidate.targetCompanyId === binding.companyId
) && (
!input.candidate.targetAgentId
|| targetSuggestion?.companyId === binding.companyId
);
let resolvedTargetAgentId: string | null = null;
if (companyScopeValid && input.candidate.route === "agent") {
const eligible = companyAgents.filter((candidate) =>
candidate.id !== binding.agentId
&& getAgentWorkEligibility({ agent: candidate, agents: companyAgents }).invokable
);
const requested = eligible.find((candidate) => candidate.id === input.candidate.targetAgentId);
resolvedTargetAgentId = requested?.id ?? eligible[0]?.id ?? null;
}
const facts: NativeAttentionFacts = {
companyScopeValid,
responseState: input.candidate.responseState ?? "none",
route: input.candidate.route === "agent" && !resolvedTargetAgentId
? "recovery"
: input.candidate.route,
summary: input.candidate.summary,
budgetExhausted: input.candidate.budgetExhausted,
governanceGate: input.candidate.governanceGate,
resolvedTargetAgentId,
};
const decision = resolveNativeAttentionStatus({
facts,
priorIssueStatus: binding.issueStatus as NativeAuthoritativeIssueStatus,
agentId: binding.agentId,
});
const auditOnly = decision.reasonCode === "attention_duplicate_suppressed";
if (auditOnly) {
const targets = await applyNativeAttentionStatusDecision({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
decision,
targetInteractionId: input.candidate.targetInteractionId,
canonicalRequestId: input.candidate.canonicalRequestId,
});
return { decision, decisionId: null, targets, resolvedTargetAgentId };
}
const assessmentId = input.assessmentId ?? binding.assessmentId;
if (!assessmentId) {
throw new NativeInteractionBridgeError(
"native_attention_assessment_missing",
"Native attention requires the server-owned work assessment for this result",
);
}
const committed = await commitNativeStatusDecision({
db: input.db,
companyId: binding.companyId,
issueId: binding.issueId,
runId: input.runId,
assessmentId,
priorStatus: binding.issueStatus,
priorStatusVersion: Number(binding.issueStatusVersion),
priorDecisionId: binding.issueDecisionId,
decision,
supersedesCommittedDecisionId: input.supersedesCommittedDecisionId,
});
return {
decision,
decisionId: committed.decision.id,
targets: [],
resolvedTargetAgentId,
};
}
/** Server-owned attention resolution from canonical routing facts. */
export function resolveNativeAttentionStatus(input: {
facts: NativeAttentionFacts;
priorIssueStatus: NativeAuthoritativeIssueStatus;
agentId: string;
}): NativeStatusDecision {
const preserve = (reasonCode: string, effects: NativeStatusEffect[]): NativeStatusDecision => ({
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
statusAction: "preserve",
toStatus: input.priorIssueStatus,
reasonCode,
unblockDescriptor: null,
effects,
});
const continuation = (reasonCode: string): NativeStatusDecision => ({
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
statusAction: "in_progress",
toStatus: "in_progress",
reasonCode,
unblockDescriptor: null,
effects: [{
kind: "enqueue_continuation",
continuationKind: input.facts.route === "agent" ? "response_wake" : "same_agent",
summary: input.facts.summary,
idempotencyKey: `native-attention:${input.facts.route}:${input.facts.summary}`,
agentId: input.agentId,
}],
});
if (!input.facts.companyScopeValid) {
return preserve("result_schema_rejected", [{
kind: "record_finalization_error",
cause: "result_schema_rejected",
nextAction: "Remove the cross-company attention target.",
agentId: input.agentId,
}]);
}
if (input.facts.responseState === "stale") {
return preserve("attention_duplicate_suppressed", [{ kind: "record_stale_response" }]);
}
if (input.facts.responseState === "expired") {
return preserve("attention_budget_exhausted", [{ kind: "record_expiry" }]);
}
if (input.facts.budgetExhausted || input.facts.route === "recovery") {
return preserve("attention_budget_exhausted", [{
kind: "record_finalization_error",
cause: "attention_budget_exhausted",
nextAction: "Assign a named recovery owner.",
agentId: input.agentId,
}]);
}
if (input.facts.route === "duplicate") {
return preserve("attention_duplicate_suppressed", [{ kind: "link_canonical_request" }]);
}
if (input.facts.governanceGate) {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
statusAction: "in_review",
toStatus: "in_review",
reasonCode: "governed_gate_pending",
unblockDescriptor: null,
effects: [
{ kind: "create_interaction", gate: input.facts.governanceGate },
{ kind: "notify_owner", agentId: input.agentId, reason: "governed_gate_pending" },
],
};
}
if (input.facts.route === "alternate_track") {
return continuation("turn_waiting_other_track_live");
}
if (input.facts.route === "context" || input.facts.route === "retry") {
return continuation("attention_resolved_from_context");
}
if (input.facts.route === "agent") {
if (!input.facts.resolvedTargetAgentId) {
return preserve("attention_budget_exhausted", [{
kind: "record_finalization_error",
cause: "attention_no_valid_route",
nextAction: "Assign a same-company eligible delegate.",
agentId: input.agentId,
}]);
}
const decision = continuation("attention_routed_to_agent");
return {
...decision,
effects: [
{ kind: "create_delegated_issue", agentId: input.facts.resolvedTargetAgentId, summary: input.facts.summary },
...decision.effects,
],
};
}
if (input.facts.route === "human") {
return {
policyVersion: NATIVE_STATUS_ARBITER_POLICY_VERSION,
statusAction: "in_review",
toStatus: "in_review",
reasonCode: "attention_requires_human_authority",
unblockDescriptor: null,
effects: [
{ kind: "create_interaction", prompt: input.facts.summary },
{ kind: "notify_owner", agentId: input.agentId, reason: "attention_requires_human_authority" },
],
};
}
throw new NativeInteractionBridgeError("native_attention_facts_invalid", "Attention facts do not select a route");
}