Merge qualified sleep review fixes and prerequisites

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* commit '74970cc21e2e5e1881516228bc7c7dbbba881a63':
  fix(retention): wake blocker retention after no-run workspace export
  Bound blocker workspace history reads by candidate workspace
  Validate eligibility before first attention retention materialization
  fix(retention): exclude finalized blockers before graph bounds
  test: hold lifecycle verification before retry identity assertions
  Exercise saved legacy input through its eligibility producer
This commit is contained in:
DottaandPaperclip committed 2026-10-31 02:01:55 -05:00
commit 2d59dd1ba3
11 files changed
+645 -135

No files matched your search

+5 -1
View File
@@ -235,7 +235,11 @@ export function attentionRetentionService(db: Db) {
where company_id = ${candidate.companyId}::uuid and id = ${row.id}::uuid`) as unknown as Array<{ at: string; due: string }>)[0];
activityAt = activity.at; dueAt = Number(activity.due);
} else {
const attention = evidence ? evidence.blockers.get(row.id) : (await issueService(database).listBlockerAttention(candidate.companyId, [row])).get(row.id);
const fresh = evidence ? undefined : await issueService(database).listBlockerAttentionBatch(candidate.companyId, [row]);
// A bounded graph cannot establish absence of live work beyond its frontier.
// Keep unknown roots visible; a later source change can revalidate them.
if (fresh?.deferredRootIds.has(row.id)) return null;
const attention = evidence ? evidence.blockers.get(row.id) : fresh!.attention.get(row.id);
if (!terminalBlockerRequiresAttention(attention)) return null;
sourceId = attention.terminalBlockerIssueId ?? row.id;
if (lock && sourceId !== candidate.sourceId) await database.execute(sql`select id from issues
+46 -3
View File
@@ -1,4 +1,8 @@
import { isProspectiveBlockedTransition } from "./routable-blocked.js";
import { requiresReviewAttention, terminalBlockerRequiresAttention, ATTENTION_AGENT_ERROR_STATUS } from "./attention-source-eligibility.js";
import { attentionIssueCondition } from "./issue-visibility.js";
import { issueService } from "./issues.js";
import { parseIssueExecutionState } from "./issue-execution-policy.js";
import { notifyRetentionWork } from "./retention-work-notifications.js";
import { decisionArchiveDeliveryService, type ArchiveNotificationBatch } from "./decision-archive-delivery.js";
import { DELIVERY_QUEUES, notifyDeliveryWork } from "./delivery-work-notifications.js";
@@ -184,17 +188,56 @@ export function decisionRetentionService(
)).then((rows) => rows[0] ?? null);
}
async function materializeSourceState(database: Db, input: { companyId: string; sourceKind: AttentionSourceKind; sourceId: string }) {
async function materializeSourceState(database: Db, input: { companyId: string; sourceKind: AttentionSourceKind; sourceId: string; actor: Pick<DecisionMutationActor, "userId"> }) {
const tables: Record<AttentionSourceKind, string> = {
approval: "approvals", join_request: "join_requests", decision: "decisions",
issue_thread_interaction: "issue_thread_interactions", recovery_action: "issue_recovery_actions",
failed_run: "heartbeat_runs", productivity_review: "issues", blocker_attention: "issues", review: "issues",
budget_alert: "budget_incidents", agent_error_alert: "agents",
};
if (input.sourceKind === "blocker_attention") {
const existingState = () => database.select({ id: decisionRetention.id }).from(decisionRetention).where(and(
eq(decisionRetention.companyId, input.companyId), eq(decisionRetention.sourceKind, input.sourceKind),
eq(decisionRetention.sourceId, input.sourceId),
)).then(rows => rows[0]);
if (input.sourceKind === "agent_error_alert") {
const [source] = await database.select().from(agents).where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.sourceId))).for("update");
if (!source || (!await existingState() && source.status !== ATTENTION_AGENT_ERROR_STATUS)) throw notFound("Attention source not found");
await decisionRetentionService(database).syncItems(input.companyId, [{
sourceKind: input.sourceKind, subject: { id: input.sourceId }, activityAt: source.updatedAt.toISOString(),
}]);
return;
}
if (["review", "blocker_attention", "productivity_review"].includes(input.sourceKind)) {
const [source] = await database.select().from(issues).where(and(eq(issues.companyId, input.companyId), eq(issues.id, input.sourceId))).for("update");
if (!source) throw notFound("Attention source not found");
const activityAt = source.unblockDescriptor?.owner === "board" && isProspectiveBlockedTransition(source)
if (!await existingState()) {
const [visible] = await database.select({ id: issues.id }).from(issues).where(and(
eq(issues.companyId, input.companyId), eq(issues.id, source.id), attentionIssueCondition(false),
));
let eligible = false;
if (visible && input.sourceKind === "review" && source.status === "in_review") {
const [pendingApproval] = await database.select({ id: approvals.id }).from(issueApprovals)
.innerJoin(approvals, eq(approvals.id, issueApprovals.approvalId)).where(and(
eq(issueApprovals.companyId, input.companyId), eq(approvals.companyId, input.companyId),
eq(issueApprovals.issueId, source.id), eq(approvals.status, "pending"),
)).limit(1);
const state = parseIssueExecutionState(source.executionState);
const attention = await issueService(database).listReviewAttention(input.companyId, [source]);
eligible = requiresReviewAttention({
hasHumanParticipant: state?.status === "pending" && state.currentParticipant?.type === "user",
hasUserAssignee: Boolean(source.assigneeUserId), hasPendingApproval: Boolean(pendingApproval),
stalled: attention.get(source.id)?.state === "stalled",
});
} else if (visible && input.sourceKind === "blocker_attention" && source.status === "blocked") {
const owner = source.unblockDescriptor?.owner;
const ownsDescriptor = owner === "board" || (owner && "userId" in owner && owner.userId === input.actor.userId);
const attention = (await issueService(database).listBlockerAttention(input.companyId, [source])).get(source.id);
eligible = Boolean(ownsDescriptor && isProspectiveBlockedTransition(source))
|| (terminalBlockerRequiresAttention(attention) && (attention.terminalBlockerIssueId ?? source.id) === source.id);
}
if (!eligible) throw notFound("Attention source not found");
}
const activityAt = input.sourceKind === "blocker_attention" && source.unblockDescriptor?.owner === "board" && isProspectiveBlockedTransition(source)
? source.blockedTransitionAt : source.updatedAt;
await decisionRetentionService(database).syncItems(input.companyId, [{
sourceKind: input.sourceKind, subject: { id: input.sourceId }, activityAt: activityAt.toISOString(),
+107 -113
View File
@@ -2520,124 +2520,87 @@ export async function listUnfinalizedExecutionWorkspaceIds(
return unfinalized;
}
/** Workspace-wide facts are shared by blockers, rather than rereading retained
* operation history for every issue pointing at the same workspace. The ordered
* OFFSET boundary keeps the rare finalize filter from turning into a company
* history scan; the existing workspace/started-at index can stream candidates. */
function blockerWorkspaceFacts(companyId: string, workspaceScope: SQL) {
return sql`(
with candidate_workspaces as materialized (
select distinct workspace_id from (${workspaceScope}) scoped_workspaces(workspace_id)
), workspace_facts as materialized (
select candidate.workspace_id, finalized.started_at as finalized_at,
legacy.phase, legacy.status, legacy.started_at
from candidate_workspaces candidate
left join lateral (
select started_at from (
select phase, status, started_at from workspace_operations
where company_id = ${companyId}::uuid
and execution_workspace_id = candidate.workspace_id
order by started_at desc offset 0
) workspace_history
where phase = 'workspace_finalize' and status = 'succeeded'
order by started_at desc limit 1
) finalized on true
left join lateral (
select phase, status, started_at from workspace_operations
where company_id = ${companyId}::uuid
and execution_workspace_id = candidate.workspace_id and issue_id is null
order by started_at desc limit 1
) legacy on true
) select * from workspace_facts
) as blocker_workspace_facts`;
}
/** An issue's own operation takes precedence over the legacy NULL fallback.
* Only a strictly later successful finalize clears either barrier. */
function pendingFinalizeBlockerCondition(companyId: string, issueId: SQL, workspaceId: SQL) {
return sql`${workspaceId} is not null and exists (
select 1 from (values (1)) seed(value)
left join lateral (
select phase, status, started_at from workspace_operations blocker_operation
where blocker_operation.company_id = ${companyId}::uuid
and blocker_operation.execution_workspace_id = ${workspaceId}
and blocker_operation.issue_id = ${issueId}
order by blocker_operation.started_at desc limit 1
) latest_blocker_operation on true
where coalesce(latest_blocker_operation.started_at, blocker_workspace_facts.started_at) is not null
and not (coalesce(latest_blocker_operation.phase, blocker_workspace_facts.phase) = 'workspace_finalize'
and coalesce(latest_blocker_operation.status, blocker_workspace_facts.status) = 'succeeded')
and (blocker_workspace_facts.finalized_at is null
or blocker_workspace_facts.finalized_at <= coalesce(latest_blocker_operation.started_at, blocker_workspace_facts.started_at))
)`;
}
async function listPendingFinalizeBlockerIssueIds(
dbOrTx: Pick<Db, "select">,
companyId: string,
blockerWorkspacePairs: Array<{
blockerIssueId: string;
executionWorkspaceId: string;
}>,
blockerWorkspacePairs: Array<{ blockerIssueId: string; executionWorkspaceId: string }>,
lockSources = false,
): Promise<Set<string>> {
const pending = new Set<string>();
const blockerIssueIds = [
...new Set(blockerWorkspacePairs.map((pair) => pair.blockerIssueId)),
];
const executionWorkspaceIds = [
...new Set(blockerWorkspacePairs.map((pair) => pair.executionWorkspaceId)),
];
if (blockerIssueIds.length === 0 || executionWorkspaceIds.length === 0)
return pending;
const blockerWorkspaceKeys = new Set(
blockerWorkspacePairs.map(
(pair) => `${pair.blockerIssueId}:${pair.executionWorkspaceId}`,
),
);
const rows: Array<{ issueId: string | null; executionWorkspaceId: string | null;
phase: string; status: string; startedAt: Date }> = [];
for (const chunk of chunkList(executionWorkspaceIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
if (lockSources) {
const pairs = [...new Map(blockerWorkspacePairs.map(pair => [`${pair.blockerIssueId}:${pair.executionWorkspaceId}`, pair])).values()];
if (lockSources) {
const workspaceIds = [...new Set(pairs.map(pair => pair.executionWorkspaceId))];
for (const chunk of chunkList(workspaceIds, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
await dbOrTx.select({ id: executionWorkspaces.id }).from(executionWorkspaces)
.where(and(eq(executionWorkspaces.companyId, companyId), inArray(executionWorkspaces.id, chunk)))
.for("update", { noWait: true });
}
const query = dbOrTx
.select({
issueId: workspaceOperations.issueId,
executionWorkspaceId: workspaceOperations.executionWorkspaceId,
phase: workspaceOperations.phase,
status: workspaceOperations.status,
startedAt: workspaceOperations.startedAt,
})
.from(workspaceOperations)
.where(
and(
eq(workspaceOperations.companyId, companyId),
inArray(
workspaceOperations.executionWorkspaceId,
chunk,
),
),
);
rows.push(...await (lockSources ? query.for("share", { noWait: true }) : query));
}
const latestAttributedByBlockerWorkspace = new Map<
string,
{ phase: string; status: string; startedAt: Date }
>();
const latestUnattributedByWorkspace = new Map<
string,
{ phase: string; status: string; startedAt: Date }
>();
const latestSuccessfulFinalizeByWorkspace = new Map<string, Date>();
for (const row of rows) {
if (!row.executionWorkspaceId) continue;
if (row.phase === "workspace_finalize" && row.status === "succeeded") {
const current = latestSuccessfulFinalizeByWorkspace.get(
row.executionWorkspaceId,
);
if (!current || row.startedAt > current) {
latestSuccessfulFinalizeByWorkspace.set(
row.executionWorkspaceId,
row.startedAt,
);
}
}
if (row.issueId) {
const key = `${row.issueId}:${row.executionWorkspaceId}`;
if (!blockerWorkspaceKeys.has(key)) continue;
const current = latestAttributedByBlockerWorkspace.get(key);
if (!current || row.startedAt > current.startedAt) {
latestAttributedByBlockerWorkspace.set(key, {
phase: row.phase,
status: row.status,
startedAt: row.startedAt,
});
}
continue;
}
const current = latestUnattributedByWorkspace.get(row.executionWorkspaceId);
if (!current || row.startedAt > current.startedAt) {
latestUnattributedByWorkspace.set(row.executionWorkspaceId, {
phase: row.phase,
status: row.status,
startedAt: row.startedAt,
});
await dbOrTx.select({ id: workspaceOperations.id }).from(workspaceOperations)
.where(and(eq(workspaceOperations.companyId, companyId), inArray(workspaceOperations.executionWorkspaceId, chunk)))
.for("share", { noWait: true });
}
}
for (const pair of blockerWorkspacePairs) {
const latest =
latestAttributedByBlockerWorkspace.get(
`${pair.blockerIssueId}:${pair.executionWorkspaceId}`,
) ?? latestUnattributedByWorkspace.get(pair.executionWorkspaceId);
if (!latest) continue; // no ops recorded -> nothing to finalize for this blocker
if (latest.phase === "workspace_finalize" && latest.status === "succeeded")
continue;
const laterSuccessfulFinalize = latestSuccessfulFinalizeByWorkspace.get(
pair.executionWorkspaceId,
);
if (laterSuccessfulFinalize && laterSuccessfulFinalize > latest.startedAt)
continue;
pending.add(pair.blockerIssueId);
// Each pair occurs in both the source and the workspace scope.
for (const chunk of chunkList(pairs, ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE / 2)) {
const values = sql.join(chunk.map(pair => sql`(${pair.blockerIssueId}::uuid, ${pair.executionWorkspaceId}::uuid)`), sql`, `);
const rows = await dbOrTx.select({ id: sql<string>`blocker_source.issue_id` })
.from(sql`(values ${values}) as blocker_source(issue_id, workspace_id)`)
.leftJoin(blockerWorkspaceFacts(companyId, sql`select workspace_id from (values ${values}) workspace_scope(issue_id, workspace_id)`),
sql`blocker_workspace_facts.workspace_id = blocker_source.workspace_id`)
.where(pendingFinalizeBlockerCondition(companyId, sql`blocker_source.issue_id`, sql`blocker_source.workspace_id`));
for (const row of rows) pending.add(row.id);
}
return pending;
}
@@ -3885,12 +3848,8 @@ async function readIssueBlockerAttentionBatch(
BLOCKER_ATTENTION_MAX_NODES * 2 - edgeCount);
if (remaining <= 0) break;
const rowLimit = remaining + 1;
const explicitBlockerRowsPromise: Promise<
IssueBlockerAttentionQueryRow[]
> = dbOrTx
const explicitBlocker = dbOrTx
.select({
issueId: issueRelations.relatedIssueId,
blockerIssueId: issues.id,
id: issues.id,
companyId: issues.companyId,
parentId: issues.parentId,
@@ -3902,14 +3861,49 @@ async function readIssueBlockerAttentionBatch(
assigneeAgentId: issues.assigneeAgentId,
assigneeUserId: issues.assigneeUserId,
})
// Resolve the globally unique relation target first. The company guard
// below remains authoritative, without a repeated company-index scan.
.from(issues).where(eq(issues.id, issueRelations.issueId))
.limit(1).as("explicit_blocker");
const workspaceScope = sql`select distinct candidate.execution_workspace_id
from issue_relations workspace_relation
inner join lateral (
select company_id, status, execution_workspace_id from issues
where id = workspace_relation.issue_id limit 1
) candidate on true
where workspace_relation.company_id = ${companyId}::uuid
and workspace_relation.type = 'blocks'
and workspace_relation.related_issue_id in (${sql.join(chunk.map(id => sql`${id}::uuid`), sql`, `)})
and candidate.company_id = ${companyId}::uuid
and candidate.status = 'done' and candidate.execution_workspace_id is not null`;
const explicitBlockerRowsPromise: Promise<
IssueBlockerAttentionQueryRow[]
> = dbOrTx
.select({
issueId: issueRelations.relatedIssueId,
blockerIssueId: explicitBlocker.id,
id: explicitBlocker.id,
companyId: explicitBlocker.companyId,
parentId: explicitBlocker.parentId,
identifier: explicitBlocker.identifier,
title: explicitBlocker.title,
status: explicitBlocker.status,
executionRunId: explicitBlocker.executionRunId,
executionWorkspaceId: explicitBlocker.executionWorkspaceId,
assigneeAgentId: explicitBlocker.assigneeAgentId,
assigneeUserId: explicitBlocker.assigneeUserId,
})
.from(issueRelations)
.innerJoin(issues, eq(issueRelations.issueId, issues.id))
.innerJoinLateral(explicitBlocker, sql`true`)
.leftJoin(blockerWorkspaceFacts(companyId, workspaceScope),
sql`blocker_workspace_facts.workspace_id = ${explicitBlocker.executionWorkspaceId}`)
.where(
and(
eq(issueRelations.companyId, companyId),
eq(issueRelations.type, "blocks"),
inArray(issueRelations.relatedIssueId, chunk),
eq(issues.companyId, companyId),
eq(explicitBlocker.companyId, companyId),
or(ne(explicitBlocker.status, "done"), pendingFinalizeBlockerCondition(companyId, sql`${explicitBlocker.id}`, sql`${explicitBlocker.executionWorkspaceId}`)),
),
).limit(rowLimit);
const childRowsPromise: Promise<IssueBlockerAttentionQueryRow[]> = dbOrTx
+8 -1
View File
@@ -1,3 +1,4 @@
import { notifyAttentionRetentionWork, notifyIssueAttentionRetentionWork } from "./retention-work-notifications.js";
import { notifyNativeResultCommit } from "./native-result-commit-notifications.js";
import { notifyNativeWorkspaceFinalization } from "./native-workspace-finalization-notifications.js";
import { notifyNativeStatusReassessment } from "./native-status-reassessment-notifications.js";
@@ -480,8 +481,14 @@ export function workspaceOperationService(db: Db) {
async recordOperation(recordInput) {
async function writeState<T>(write: (database: Db) => Promise<T>): Promise<T> {
if (recordInput.phase !== "workspace_finalize" || !input.heartbeatRunId) return write(db);
if (recordInput.phase !== "workspace_finalize" || (!input.heartbeatRunId && !executionWorkspaceId && !input.issueId)) return write(db);
return db.transaction(async tx => {
if (!input.heartbeatRunId) {
// An export clears barriers for every issue sharing this workspace.
if (executionWorkspaceId) await notifyAttentionRetentionWork(tx, { companyId: input.companyId, sourceKind: "blocker_attention" });
else await notifyIssueAttentionRetentionWork(tx, { companyId: input.companyId, issueId: input.issueId ?? undefined });
return write(tx as unknown as Db);
}
await notifyNativeResultCommit(tx, { companyId: input.companyId,
runId: input.heartbeatRunId!, issueId: input.issueId ?? undefined });
await notifyNativeWorkspaceFinalization(tx, { companyId: input.companyId,