authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-13 18:10:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-13 18:10:13-07:00
logc9b07d665d11ff19fdb655be56a014cd60d9166b
treeac0d1dc34861d2554cfde14f70a7438b54032292
parent2e42f3fdf624adb24a14cf6d2f31875014ebf914
signature Signed by PGP key B5690EEEBB952194

Backfill projected shell summaries and stale approval cleanup (#2004)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

5 files changed, 859 insertions(+), 0 deletions(-)

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+323
......@@ -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+39
......@@ -90,6 +90,17 @@ function extractActivityRequestId(payload: unknown): ApprovalRequestId | null {
9090 return typeof requestId === "string" ? ApprovalRequestId.make(requestId) : null;
9191}
9292
93function 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
93104function 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+2
......@@ -36,6 +36,7 @@ import Migration0020 from "./Migrations/020_AuthAccessManagement.ts";
3636import Migration0021 from "./Migrations/021_AuthSessionClientMetadata.ts";
3737import Migration0022 from "./Migrations/022_AuthSessionLastConnectedAt.ts";
3838import Migration0023 from "./Migrations/023_ProjectionThreadShellSummary.ts";
39import 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
7678export const makeMigrationLoader = (throughId?: number) =>
apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts created+218
......@@ -0,0 +1,218 @@
1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
3import * as SqlClient from "effect/unstable/sql/SqlClient";
4
5import { runMigrations } from "../Migrations.ts";
6import * as NodeSqliteClient from "../NodeSqliteClient.ts";
7
8const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory()));
9
10layer("024_BackfillProjectionThreadShellSummary", (it) => {
11 it.effect("backfills thread shell summary fields and clears stale projected approvals", () =>
12 Effect.gen(function* () {
13 const sql = yield* SqlClient.SqlClient;
14
15 yield* runMigrations({ toMigrationInclusive: 23 });
16
17 yield* sql`
18 INSERT INTO projection_threads (
19 thread_id,
20 project_id,
21 title,
22 model_selection_json,
23 runtime_mode,
24 interaction_mode,
25 branch,
26 worktree_path,
27 latest_turn_id,
28 created_at,
29 updated_at,
30 archived_at,
31 latest_user_message_at,
32 pending_approval_count,
33 pending_user_input_count,
34 has_actionable_proposed_plan,
35 deleted_at
36 )
37 VALUES (
38 'thread-1',
39 'project-1',
40 'Thread 1',
41 '{"provider":"codex","model":"gpt-5-codex"}',
42 'approval-required',
43 'plan',
44 NULL,
45 NULL,
46 'turn-1',
47 '2026-02-24T00:00:00.000Z',
48 '2026-02-24T00:00:00.000Z',
49 NULL,
50 NULL,
51 0,
52 0,
53 0,
54 NULL
55 )
56 `;
57
58 yield* sql`
59 INSERT INTO projection_thread_messages (
60 message_id,
61 thread_id,
62 turn_id,
63 role,
64 text,
65 attachments_json,
66 is_streaming,
67 created_at,
68 updated_at
69 )
70 VALUES (
71 'message-user-1',
72 'thread-1',
73 'turn-1',
74 'user',
75 'Need help',
76 NULL,
77 0,
78 '2026-02-24T00:01:00.000Z',
79 '2026-02-24T00:01:00.000Z'
80 )
81 `;
82
83 yield* sql`
84 INSERT INTO projection_thread_activities (
85 activity_id,
86 thread_id,
87 turn_id,
88 tone,
89 kind,
90 summary,
91 payload_json,
92 sequence,
93 created_at
94 )
95 VALUES
96 (
97 'activity-approval-requested',
98 'thread-1',
99 'turn-1',
100 'approval',
101 'approval.requested',
102 'Command approval requested',
103 '{"requestId":"approval-1","requestKind":"command"}',
104 NULL,
105 '2026-02-24T00:02:00.000Z'
106 ),
107 (
108 'activity-approval-stale',
109 'thread-1',
110 'turn-1',
111 'error',
112 'provider.approval.respond.failed',
113 'Provider approval response failed',
114 '{"requestId":"approval-1","detail":"Unknown pending permission request: approval-1"}',
115 NULL,
116 '2026-02-24T00:03:00.000Z'
117 ),
118 (
119 'activity-user-input-requested',
120 'thread-1',
121 'turn-1',
122 'info',
123 'user-input.requested',
124 'User input requested',
125 '{"requestId":"input-1","questions":[{"id":"area","header":"Area","question":"Which repo area should I inspect next?","options":[{"label":"Server","description":"Server orchestration."}]}]}',
126 NULL,
127 '2026-02-24T00:04:00.000Z'
128 )
129 `;
130
131 yield* sql`
132 INSERT INTO projection_thread_proposed_plans (
133 plan_id,
134 thread_id,
135 turn_id,
136 plan_markdown,
137 implemented_at,
138 implementation_thread_id,
139 created_at,
140 updated_at
141 )
142 VALUES (
143 'plan-1',
144 'thread-1',
145 'turn-1',
146 '# Do the thing',
147 NULL,
148 NULL,
149 '2026-02-24T00:05:00.000Z',
150 '2026-02-24T00:05:00.000Z'
151 )
152 `;
153
154 yield* sql`
155 INSERT INTO projection_pending_approvals (
156 request_id,
157 thread_id,
158 turn_id,
159 status,
160 decision,
161 created_at,
162 resolved_at
163 )
164 VALUES (
165 'approval-1',
166 'thread-1',
167 'turn-1',
168 'pending',
169 NULL,
170 '2026-02-24T00:02:00.000Z',
171 NULL
172 )
173 `;
174
175 yield* runMigrations({ toMigrationInclusive: 24 });
176
177 const threadRows = yield* sql<{
178 readonly latestUserMessageAt: string | null;
179 readonly pendingApprovalCount: number;
180 readonly pendingUserInputCount: number;
181 readonly hasActionableProposedPlan: number;
182 }>`
183 SELECT
184 latest_user_message_at AS "latestUserMessageAt",
185 pending_approval_count AS "pendingApprovalCount",
186 pending_user_input_count AS "pendingUserInputCount",
187 has_actionable_proposed_plan AS "hasActionableProposedPlan"
188 FROM projection_threads
189 WHERE thread_id = 'thread-1'
190 `;
191 assert.deepStrictEqual(threadRows, [
192 {
193 latestUserMessageAt: "2026-02-24T00:01:00.000Z",
194 pendingApprovalCount: 0,
195 pendingUserInputCount: 1,
196 hasActionableProposedPlan: 1,
197 },
198 ]);
199
200 const approvalRows = yield* sql<{
201 readonly status: string;
202 readonly resolvedAt: string | null;
203 }>`
204 SELECT
205 status,
206 resolved_at AS "resolvedAt"
207 FROM projection_pending_approvals
208 WHERE request_id = 'approval-1'
209 `;
210 assert.deepStrictEqual(approvalRows, [
211 {
212 status: "resolved",
213 resolvedAt: "2026-02-24T00:03:00.000Z",
214 },
215 ]);
216 }),
217 );
218});
apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.ts created+277
......@@ -0,0 +1,277 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as Effect from "effect/Effect";
3
4export default Effect.gen(function* () {
5 const sql = yield* SqlClient.SqlClient;
6
7 yield* sql`
8 INSERT OR IGNORE INTO projection_pending_approvals (
9 request_id,
10 thread_id,
11 turn_id,
12 status,
13 decision,
14 created_at,
15 resolved_at
16 )
17 SELECT
18 requested.request_id,
19 requested.thread_id,
20 requested.turn_id,
21 'pending',
22 NULL,
23 requested.created_at,
24 NULL
25 FROM (
26 SELECT
27 json_extract(payload_json, '$.requestId') AS request_id,
28 thread_id,
29 turn_id,
30 created_at,
31 ROW_NUMBER() OVER (
32 PARTITION BY json_extract(payload_json, '$.requestId')
33 ORDER BY created_at ASC, activity_id ASC
34 ) AS row_number
35 FROM projection_thread_activities
36 WHERE kind = 'approval.requested'
37 AND json_extract(payload_json, '$.requestId') IS NOT NULL
38 ) AS requested
39 WHERE requested.row_number = 1
40 `;
41
42 yield* sql`
43 WITH latest_resolutions AS (
44 SELECT
45 resolved.request_id,
46 resolved.resolved_at,
47 resolved.decision
48 FROM (
49 SELECT
50 json_extract(payload_json, '$.requestId') AS request_id,
51 created_at AS resolved_at,
52 CASE
53 WHEN json_extract(payload_json, '$.decision') IN (
54 'accept',
55 'acceptForSession',
56 'decline',
57 'cancel'
58 )
59 THEN json_extract(payload_json, '$.decision')
60 ELSE NULL
61 END AS decision,
62 ROW_NUMBER() OVER (
63 PARTITION BY json_extract(payload_json, '$.requestId')
64 ORDER BY created_at DESC, activity_id DESC
65 ) AS row_number
66 FROM projection_thread_activities
67 WHERE kind = 'approval.resolved'
68 AND json_extract(payload_json, '$.requestId') IS NOT NULL
69 ) AS resolved
70 WHERE resolved.row_number = 1
71 )
72 UPDATE projection_pending_approvals
73 SET
74 status = 'resolved',
75 decision = (
76 SELECT latest_resolutions.decision
77 FROM latest_resolutions
78 WHERE latest_resolutions.request_id = projection_pending_approvals.request_id
79 ),
80 resolved_at = (
81 SELECT latest_resolutions.resolved_at
82 FROM latest_resolutions
83 WHERE latest_resolutions.request_id = projection_pending_approvals.request_id
84 )
85 WHERE EXISTS (
86 SELECT 1
87 FROM latest_resolutions
88 WHERE latest_resolutions.request_id = projection_pending_approvals.request_id
89 )
90 `;
91
92 yield* sql`
93 WITH latest_response_events AS (
94 SELECT
95 response.request_id,
96 response.resolved_at,
97 response.decision
98 FROM (
99 SELECT
100 json_extract(payload_json, '$.requestId') AS request_id,
101 occurred_at AS resolved_at,
102 CASE
103 WHEN json_extract(payload_json, '$.decision') IN (
104 'accept',
105 'acceptForSession',
106 'decline',
107 'cancel'
108 )
109 THEN json_extract(payload_json, '$.decision')
110 ELSE NULL
111 END AS decision,
112 ROW_NUMBER() OVER (
113 PARTITION BY json_extract(payload_json, '$.requestId')
114 ORDER BY occurred_at DESC, sequence DESC
115 ) AS row_number
116 FROM orchestration_events
117 WHERE event_type = 'thread.approval-response-requested'
118 AND json_extract(payload_json, '$.requestId') IS NOT NULL
119 ) AS response
120 WHERE response.row_number = 1
121 )
122 UPDATE projection_pending_approvals
123 SET
124 status = 'resolved',
125 decision = (
126 SELECT latest_response_events.decision
127 FROM latest_response_events
128 WHERE latest_response_events.request_id = projection_pending_approvals.request_id
129 ),
130 resolved_at = (
131 SELECT latest_response_events.resolved_at
132 FROM latest_response_events
133 WHERE latest_response_events.request_id = projection_pending_approvals.request_id
134 )
135 WHERE EXISTS (
136 SELECT 1
137 FROM latest_response_events
138 WHERE latest_response_events.request_id = projection_pending_approvals.request_id
139 )
140 `;
141
142 yield* sql`
143 WITH latest_stale_failures AS (
144 SELECT
145 failure.request_id,
146 failure.resolved_at
147 FROM (
148 SELECT
149 json_extract(payload_json, '$.requestId') AS request_id,
150 created_at AS resolved_at,
151 ROW_NUMBER() OVER (
152 PARTITION BY json_extract(payload_json, '$.requestId')
153 ORDER BY created_at DESC, activity_id DESC
154 ) AS row_number
155 FROM projection_thread_activities
156 WHERE kind = 'provider.approval.respond.failed'
157 AND json_extract(payload_json, '$.requestId') IS NOT NULL
158 AND (
159 lower(COALESCE(json_extract(payload_json, '$.detail'), ''))
160 LIKE '%stale pending approval request%'
161 OR lower(COALESCE(json_extract(payload_json, '$.detail'), ''))
162 LIKE '%unknown pending approval request%'
163 OR lower(COALESCE(json_extract(payload_json, '$.detail'), ''))
164 LIKE '%unknown pending permission request%'
165 )
166 ) AS failure
167 WHERE failure.row_number = 1
168 )
169 UPDATE projection_pending_approvals
170 SET
171 status = 'resolved',
172 decision = NULL,
173 resolved_at = (
174 SELECT latest_stale_failures.resolved_at
175 FROM latest_stale_failures
176 WHERE latest_stale_failures.request_id = projection_pending_approvals.request_id
177 )
178 WHERE status = 'pending'
179 AND EXISTS (
180 SELECT 1
181 FROM latest_stale_failures
182 WHERE latest_stale_failures.request_id = projection_pending_approvals.request_id
183 )
184 `;
185
186 yield* sql`
187 UPDATE projection_threads
188 SET
189 latest_user_message_at = (
190 SELECT MAX(message.created_at)
191 FROM projection_thread_messages AS message
192 WHERE message.thread_id = projection_threads.thread_id
193 AND message.role = 'user'
194 ),
195 pending_approval_count = COALESCE((
196 SELECT COUNT(*)
197 FROM projection_pending_approvals
198 WHERE projection_pending_approvals.thread_id = projection_threads.thread_id
199 AND projection_pending_approvals.status = 'pending'
200 ), 0),
201 pending_user_input_count = COALESCE((
202 WITH latest_user_input_states AS (
203 SELECT
204 latest.request_id,
205 latest.kind,
206 latest.detail
207 FROM (
208 SELECT
209 json_extract(activity.payload_json, '$.requestId') AS request_id,
210 activity.kind,
211 lower(COALESCE(json_extract(activity.payload_json, '$.detail'), '')) AS detail,
212 ROW_NUMBER() OVER (
213 PARTITION BY json_extract(activity.payload_json, '$.requestId')
214 ORDER BY activity.created_at DESC, activity.activity_id DESC
215 ) AS row_number
216 FROM projection_thread_activities AS activity
217 WHERE activity.thread_id = projection_threads.thread_id
218 AND json_extract(activity.payload_json, '$.requestId') IS NOT NULL
219 AND activity.kind IN (
220 'user-input.requested',
221 'user-input.resolved',
222 'provider.user-input.respond.failed'
223 )
224 ) AS latest
225 WHERE latest.row_number = 1
226 )
227 SELECT COUNT(*)
228 FROM latest_user_input_states
229 WHERE latest_user_input_states.kind = 'user-input.requested'
230 OR (
231 latest_user_input_states.kind = 'provider.user-input.respond.failed'
232 AND latest_user_input_states.detail NOT LIKE '%stale pending user-input request%'
233 AND latest_user_input_states.detail NOT LIKE '%unknown pending user-input request%'
234 )
235 ), 0),
236 has_actionable_proposed_plan = COALESCE((
237 SELECT CASE
238 WHEN projection_threads.latest_turn_id IS NOT NULL
239 AND EXISTS (
240 SELECT 1
241 FROM projection_thread_proposed_plans AS latest_turn_plan_exists
242 WHERE latest_turn_plan_exists.thread_id = projection_threads.thread_id
243 AND latest_turn_plan_exists.turn_id = projection_threads.latest_turn_id
244 )
245 THEN CASE
246 WHEN (
247 SELECT latest_turn_plan.implemented_at
248 FROM projection_thread_proposed_plans AS latest_turn_plan
249 WHERE latest_turn_plan.thread_id = projection_threads.thread_id
250 AND latest_turn_plan.turn_id = projection_threads.latest_turn_id
251 ORDER BY latest_turn_plan.updated_at DESC, latest_turn_plan.plan_id DESC
252 LIMIT 1
253 ) IS NULL
254 THEN 1
255 ELSE 0
256 END
257 WHEN EXISTS (
258 SELECT 1
259 FROM projection_thread_proposed_plans AS any_plan
260 WHERE any_plan.thread_id = projection_threads.thread_id
261 )
262 THEN CASE
263 WHEN (
264 SELECT latest_plan.implemented_at
265 FROM projection_thread_proposed_plans AS latest_plan
266 WHERE latest_plan.thread_id = projection_threads.thread_id
267 ORDER BY latest_plan.updated_at DESC, latest_plan.plan_id DESC
268 LIMIT 1
269 ) IS NULL
270 THEN 1
271 ELSE 0
272 END
273 ELSE 0
274 END
275 ), 0)
276 `;
277});