Skip to content

Commit c9b07d6

Browse files
Backfill projected shell summaries and stale approval cleanup (#2004)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 2e42f3f commit c9b07d6

File tree

5 files changed

+859
-0
lines changed

5 files changed

+859
-0
lines changed

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,6 +1505,329 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
15051505
}),
15061506
);
15071507

1508+
it.effect("clears stale pending approvals from projected shell summaries", () =>
1509+
Effect.gen(function* () {
1510+
const projectionPipeline = yield* OrchestrationProjectionPipeline;
1511+
const eventStore = yield* OrchestrationEventStore;
1512+
const sql = yield* SqlClient.SqlClient;
1513+
const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
1514+
eventStore
1515+
.append(event)
1516+
.pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent)));
1517+
1518+
yield* appendAndProject({
1519+
type: "project.created",
1520+
eventId: EventId.make("evt-stale-approval-1"),
1521+
aggregateKind: "project",
1522+
aggregateId: ProjectId.make("project-stale-approval"),
1523+
occurredAt: "2026-02-26T12:30:00.000Z",
1524+
commandId: CommandId.make("cmd-stale-approval-1"),
1525+
causationEventId: null,
1526+
correlationId: CorrelationId.make("cmd-stale-approval-1"),
1527+
metadata: {},
1528+
payload: {
1529+
projectId: ProjectId.make("project-stale-approval"),
1530+
title: "Project Stale Approval",
1531+
workspaceRoot: "/tmp/project-stale-approval",
1532+
defaultModelSelection: null,
1533+
scripts: [],
1534+
createdAt: "2026-02-26T12:30:00.000Z",
1535+
updatedAt: "2026-02-26T12:30:00.000Z",
1536+
},
1537+
});
1538+
1539+
yield* appendAndProject({
1540+
type: "thread.created",
1541+
eventId: EventId.make("evt-stale-approval-2"),
1542+
aggregateKind: "thread",
1543+
aggregateId: ThreadId.make("thread-stale-approval"),
1544+
occurredAt: "2026-02-26T12:30:01.000Z",
1545+
commandId: CommandId.make("cmd-stale-approval-2"),
1546+
causationEventId: null,
1547+
correlationId: CorrelationId.make("cmd-stale-approval-2"),
1548+
metadata: {},
1549+
payload: {
1550+
threadId: ThreadId.make("thread-stale-approval"),
1551+
projectId: ProjectId.make("project-stale-approval"),
1552+
title: "Thread Stale Approval",
1553+
modelSelection: {
1554+
provider: "codex",
1555+
model: "gpt-5-codex",
1556+
},
1557+
runtimeMode: "approval-required",
1558+
interactionMode: "default",
1559+
branch: null,
1560+
worktreePath: null,
1561+
createdAt: "2026-02-26T12:30:01.000Z",
1562+
updatedAt: "2026-02-26T12:30:01.000Z",
1563+
},
1564+
});
1565+
1566+
yield* appendAndProject({
1567+
type: "thread.activity-appended",
1568+
eventId: EventId.make("evt-stale-approval-3"),
1569+
aggregateKind: "thread",
1570+
aggregateId: ThreadId.make("thread-stale-approval"),
1571+
occurredAt: "2026-02-26T12:30:02.000Z",
1572+
commandId: CommandId.make("cmd-stale-approval-3"),
1573+
causationEventId: null,
1574+
correlationId: CorrelationId.make("cmd-stale-approval-3"),
1575+
metadata: {},
1576+
payload: {
1577+
threadId: ThreadId.make("thread-stale-approval"),
1578+
activity: {
1579+
id: EventId.make("activity-stale-approval-requested"),
1580+
tone: "approval",
1581+
kind: "approval.requested",
1582+
summary: "Command approval requested",
1583+
payload: {
1584+
requestId: "approval-request-stale-1",
1585+
requestKind: "command",
1586+
},
1587+
turnId: null,
1588+
createdAt: "2026-02-26T12:30:02.000Z",
1589+
},
1590+
},
1591+
});
1592+
1593+
yield* appendAndProject({
1594+
type: "thread.activity-appended",
1595+
eventId: EventId.make("evt-stale-approval-4"),
1596+
aggregateKind: "thread",
1597+
aggregateId: ThreadId.make("thread-stale-approval"),
1598+
occurredAt: "2026-02-26T12:30:03.000Z",
1599+
commandId: CommandId.make("cmd-stale-approval-4"),
1600+
causationEventId: null,
1601+
correlationId: CorrelationId.make("cmd-stale-approval-4"),
1602+
metadata: {},
1603+
payload: {
1604+
threadId: ThreadId.make("thread-stale-approval"),
1605+
activity: {
1606+
id: EventId.make("activity-stale-approval-failed"),
1607+
tone: "error",
1608+
kind: "provider.approval.respond.failed",
1609+
summary: "Provider approval response failed",
1610+
payload: {
1611+
requestId: "approval-request-stale-1",
1612+
detail: "Unknown pending permission request: approval-request-stale-1",
1613+
},
1614+
turnId: null,
1615+
createdAt: "2026-02-26T12:30:03.000Z",
1616+
},
1617+
},
1618+
});
1619+
1620+
const approvalRows = yield* sql<{
1621+
readonly requestId: string;
1622+
readonly status: string;
1623+
readonly resolvedAt: string | null;
1624+
}>`
1625+
SELECT
1626+
request_id AS "requestId",
1627+
status,
1628+
resolved_at AS "resolvedAt"
1629+
FROM projection_pending_approvals
1630+
WHERE request_id = 'approval-request-stale-1'
1631+
`;
1632+
assert.deepEqual(approvalRows, [
1633+
{
1634+
requestId: "approval-request-stale-1",
1635+
status: "resolved",
1636+
resolvedAt: "2026-02-26T12:30:03.000Z",
1637+
},
1638+
]);
1639+
1640+
const threadRows = yield* sql<{
1641+
readonly pendingApprovalCount: number;
1642+
}>`
1643+
SELECT pending_approval_count AS "pendingApprovalCount"
1644+
FROM projection_threads
1645+
WHERE thread_id = 'thread-stale-approval'
1646+
`;
1647+
assert.deepEqual(threadRows, [{ pendingApprovalCount: 0 }]);
1648+
}),
1649+
);
1650+
1651+
it.effect("ignores non-stale provider approval response failures", () =>
1652+
Effect.gen(function* () {
1653+
const projectionPipeline = yield* OrchestrationProjectionPipeline;
1654+
const eventStore = yield* OrchestrationEventStore;
1655+
const sql = yield* SqlClient.SqlClient;
1656+
const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
1657+
eventStore
1658+
.append(event)
1659+
.pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent)));
1660+
1661+
yield* appendAndProject({
1662+
type: "project.created",
1663+
eventId: EventId.make("evt-nonstale-approval-1"),
1664+
aggregateKind: "project",
1665+
aggregateId: ProjectId.make("project-nonstale-approval"),
1666+
occurredAt: "2026-02-26T12:45:00.000Z",
1667+
commandId: CommandId.make("cmd-nonstale-approval-1"),
1668+
causationEventId: null,
1669+
correlationId: CorrelationId.make("cmd-nonstale-approval-1"),
1670+
metadata: {},
1671+
payload: {
1672+
projectId: ProjectId.make("project-nonstale-approval"),
1673+
title: "Project Non-Stale Approval",
1674+
workspaceRoot: "/tmp/project-nonstale-approval",
1675+
defaultModelSelection: null,
1676+
scripts: [],
1677+
createdAt: "2026-02-26T12:45:00.000Z",
1678+
updatedAt: "2026-02-26T12:45:00.000Z",
1679+
},
1680+
});
1681+
1682+
yield* appendAndProject({
1683+
type: "thread.created",
1684+
eventId: EventId.make("evt-nonstale-approval-2"),
1685+
aggregateKind: "thread",
1686+
aggregateId: ThreadId.make("thread-nonstale-approval"),
1687+
occurredAt: "2026-02-26T12:45:01.000Z",
1688+
commandId: CommandId.make("cmd-nonstale-approval-2"),
1689+
causationEventId: null,
1690+
correlationId: CorrelationId.make("cmd-nonstale-approval-2"),
1691+
metadata: {},
1692+
payload: {
1693+
threadId: ThreadId.make("thread-nonstale-approval"),
1694+
projectId: ProjectId.make("project-nonstale-approval"),
1695+
title: "Thread Non-Stale Approval",
1696+
modelSelection: {
1697+
provider: "codex",
1698+
model: "gpt-5-codex",
1699+
},
1700+
runtimeMode: "approval-required",
1701+
interactionMode: "default",
1702+
branch: null,
1703+
worktreePath: null,
1704+
createdAt: "2026-02-26T12:45:01.000Z",
1705+
updatedAt: "2026-02-26T12:45:01.000Z",
1706+
},
1707+
});
1708+
1709+
yield* appendAndProject({
1710+
type: "thread.activity-appended",
1711+
eventId: EventId.make("evt-nonstale-approval-3"),
1712+
aggregateKind: "thread",
1713+
aggregateId: ThreadId.make("thread-nonstale-approval"),
1714+
occurredAt: "2026-02-26T12:45:02.000Z",
1715+
commandId: CommandId.make("cmd-nonstale-approval-3"),
1716+
causationEventId: null,
1717+
correlationId: CorrelationId.make("cmd-nonstale-approval-3"),
1718+
metadata: {},
1719+
payload: {
1720+
threadId: ThreadId.make("thread-nonstale-approval"),
1721+
activity: {
1722+
id: EventId.make("activity-nonstale-approval-requested"),
1723+
tone: "approval",
1724+
kind: "approval.requested",
1725+
summary: "Command approval requested",
1726+
payload: {
1727+
requestId: "approval-request-nonstale-existing",
1728+
requestKind: "command",
1729+
},
1730+
turnId: null,
1731+
createdAt: "2026-02-26T12:45:02.000Z",
1732+
},
1733+
},
1734+
});
1735+
1736+
yield* appendAndProject({
1737+
type: "thread.activity-appended",
1738+
eventId: EventId.make("evt-nonstale-approval-4"),
1739+
aggregateKind: "thread",
1740+
aggregateId: ThreadId.make("thread-nonstale-approval"),
1741+
occurredAt: "2026-02-26T12:45:03.000Z",
1742+
commandId: CommandId.make("cmd-nonstale-approval-4"),
1743+
causationEventId: null,
1744+
correlationId: CorrelationId.make("cmd-nonstale-approval-4"),
1745+
metadata: {},
1746+
payload: {
1747+
threadId: ThreadId.make("thread-nonstale-approval"),
1748+
activity: {
1749+
id: EventId.make("activity-nonstale-approval-failed-existing"),
1750+
tone: "error",
1751+
kind: "provider.approval.respond.failed",
1752+
summary: "Provider approval response failed",
1753+
payload: {
1754+
requestId: "approval-request-nonstale-existing",
1755+
detail: "Provider timed out while responding to approval request",
1756+
},
1757+
turnId: TurnId.make("turn-nonstale-failure"),
1758+
createdAt: "2026-02-26T12:45:03.000Z",
1759+
},
1760+
},
1761+
});
1762+
1763+
yield* appendAndProject({
1764+
type: "thread.activity-appended",
1765+
eventId: EventId.make("evt-nonstale-approval-5"),
1766+
aggregateKind: "thread",
1767+
aggregateId: ThreadId.make("thread-nonstale-approval"),
1768+
occurredAt: "2026-02-26T12:45:04.000Z",
1769+
commandId: CommandId.make("cmd-nonstale-approval-5"),
1770+
causationEventId: null,
1771+
correlationId: CorrelationId.make("cmd-nonstale-approval-5"),
1772+
metadata: {},
1773+
payload: {
1774+
threadId: ThreadId.make("thread-nonstale-approval"),
1775+
activity: {
1776+
id: EventId.make("activity-nonstale-approval-failed-missing"),
1777+
tone: "error",
1778+
kind: "provider.approval.respond.failed",
1779+
summary: "Provider approval response failed",
1780+
payload: {
1781+
requestId: "approval-request-nonstale-missing",
1782+
detail: "Provider timed out while responding to approval request",
1783+
},
1784+
turnId: null,
1785+
createdAt: "2026-02-26T12:45:04.000Z",
1786+
},
1787+
},
1788+
});
1789+
1790+
const approvalRows = yield* sql<{
1791+
readonly requestId: string;
1792+
readonly status: string;
1793+
readonly turnId: string | null;
1794+
readonly createdAt: string;
1795+
readonly resolvedAt: string | null;
1796+
}>`
1797+
SELECT
1798+
request_id AS "requestId",
1799+
status,
1800+
turn_id AS "turnId",
1801+
created_at AS "createdAt",
1802+
resolved_at AS "resolvedAt"
1803+
FROM projection_pending_approvals
1804+
WHERE request_id IN (
1805+
'approval-request-nonstale-existing',
1806+
'approval-request-nonstale-missing'
1807+
)
1808+
ORDER BY request_id
1809+
`;
1810+
assert.deepEqual(approvalRows, [
1811+
{
1812+
requestId: "approval-request-nonstale-existing",
1813+
status: "pending",
1814+
turnId: null,
1815+
createdAt: "2026-02-26T12:45:02.000Z",
1816+
resolvedAt: null,
1817+
},
1818+
]);
1819+
1820+
const threadRows = yield* sql<{
1821+
readonly pendingApprovalCount: number;
1822+
}>`
1823+
SELECT pending_approval_count AS "pendingApprovalCount"
1824+
FROM projection_threads
1825+
WHERE thread_id = 'thread-nonstale-approval'
1826+
`;
1827+
assert.deepEqual(threadRows, [{ pendingApprovalCount: 1 }]);
1828+
}),
1829+
);
1830+
15081831
it.effect("does not fallback-retain messages whose turnId is removed by revert", () =>
15091832
Effect.gen(function* () {
15101833
const projectionPipeline = yield* OrchestrationProjectionPipeline;

apps/server/src/orchestration/Layers/ProjectionPipeline.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@ function extractActivityRequestId(payload: unknown): ApprovalRequestId | null {
9090
return typeof requestId === "string" ? ApprovalRequestId.make(requestId) : null;
9191
}
9292

93+
function isStalePendingApprovalFailureDetail(detail: string | null): boolean {
94+
if (detail === null) {
95+
return false;
96+
}
97+
return (
98+
detail.includes("stale pending approval request") ||
99+
detail.includes("unknown pending approval request") ||
100+
detail.includes("unknown pending permission request")
101+
);
102+
}
103+
93104
function derivePendingUserInputCountFromActivities(
94105
activities: ReadonlyArray<ProjectionThreadActivity>,
95106
): number {
@@ -1245,6 +1256,34 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
12451256
});
12461257
return;
12471258
}
1259+
if (event.payload.activity.kind === "provider.approval.respond.failed") {
1260+
const payload =
1261+
typeof event.payload.activity.payload === "object" &&
1262+
event.payload.activity.payload !== null
1263+
? (event.payload.activity.payload as Record<string, unknown>)
1264+
: null;
1265+
const detail =
1266+
typeof payload?.detail === "string" ? payload.detail.toLowerCase() : null;
1267+
if (isStalePendingApprovalFailureDetail(detail)) {
1268+
if (Option.isNone(existingRow)) {
1269+
return;
1270+
}
1271+
if (existingRow.value.status === "resolved") {
1272+
return;
1273+
}
1274+
yield* projectionPendingApprovalRepository.upsert({
1275+
requestId,
1276+
threadId: existingRow.value.threadId,
1277+
turnId: existingRow.value.turnId,
1278+
status: "resolved",
1279+
decision: null,
1280+
createdAt: existingRow.value.createdAt,
1281+
resolvedAt: event.payload.activity.createdAt,
1282+
});
1283+
return;
1284+
}
1285+
return;
1286+
}
12481287
if (Option.isSome(existingRow) && existingRow.value.status === "resolved") {
12491288
return;
12501289
}

apps/server/src/persistence/Migrations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import Migration0020 from "./Migrations/020_AuthAccessManagement.ts";
3636
import Migration0021 from "./Migrations/021_AuthSessionClientMetadata.ts";
3737
import Migration0022 from "./Migrations/022_AuthSessionLastConnectedAt.ts";
3838
import Migration0023 from "./Migrations/023_ProjectionThreadShellSummary.ts";
39+
import Migration0024 from "./Migrations/024_BackfillProjectionThreadShellSummary.ts";
3940

4041
/**
4142
* Migration loader with all migrations defined inline.
@@ -71,6 +72,7 @@ export const migrationEntries = [
7172
[21, "AuthSessionClientMetadata", Migration0021],
7273
[22, "AuthSessionLastConnectedAt", Migration0022],
7374
[23, "ProjectionThreadShellSummary", Migration0023],
75+
[24, "BackfillProjectionThreadShellSummary", Migration0024],
7476
] as const;
7577

7678
export const makeMigrationLoader = (throughId?: number) =>

0 commit comments

Comments
 (0)